@@ -16,13 +16,29 @@ import (
1616// for query costs compared to baseline (10% = queries can cost up to 110% of baseline)
1717const DefaultCostThresholdPercent = 10.0
1818
19- // Baseline stores the EXPLAIN analysis results for a query
20- type Baseline struct {
21- Query string `json:"query"`
22- Timestamp string `json:"timestamp"`
23- Plan map [string ]any `json:"plan"`
24- PlanSignature * PlanSignature `json:"plan_signature,omitempty"` // Optional for backwards compatibility
25- }
19+ type (
20+ Baseline struct {
21+ Query string `json:"query"`
22+ Timestamp string `json:"timestamp"`
23+ Plan map [string ]any `json:"plan"`
24+ PlanSignature * PlanSignature `json:"plan_signature,omitempty"`
25+ AnalyzeMode bool `json:"analyze_mode,omitempty"`
26+ Buffers * BufferBaseline `json:"buffers,omitempty"`
27+ Actuals * ActualBaseline `json:"actuals,omitempty"`
28+ }
29+
30+ BufferBaseline struct {
31+ SharedHitBlocks int64 `json:"shared_hit_blocks"`
32+ SharedReadBlocks int64 `json:"shared_read_blocks"`
33+ TotalBuffers int64 `json:"total_buffers"`
34+ }
35+
36+ ActualBaseline struct {
37+ ActualRows float64 `json:"actual_rows"`
38+ PlanRows float64 `json:"plan_rows"`
39+ ExecutionTimeMs float64 `json:"execution_time_ms"`
40+ }
41+ )
2642
2743// GetBaselinePath returns the path where baseline JSON file should be stored
2844func getBaselinePath (q * Query , baselineDir string , bindingName string ) string {
@@ -91,7 +107,7 @@ func ExecuteExplainWithOptions(q Querier, query string, opts ExplainOptions, arg
91107 return & plans [0 ], nil
92108}
93109
94- func (q * Query ) CreateBaseline (baselineDir string , planDir string , db * sql.DB ) error {
110+ func (q * Query ) CreateBaseline (baselineDir string , planDir string , db * sql.DB , useAnalyze bool ) error {
95111 var plan * Plan
96112 var err error
97113
@@ -108,7 +124,7 @@ func (q *Query) CreateBaseline(baselineDir string, planDir string, db *sql.DB) e
108124 }
109125 }
110126
111- baselines , fullPlans , err := plan .CreateBaselines (db )
127+ baselines , fullPlans , err := plan .CreateBaselines (db , useAnalyze )
112128 if err != nil {
113129 return err
114130 }
@@ -119,15 +135,15 @@ func (q *Query) CreateBaseline(baselineDir string, planDir string, db *sql.DB) e
119135 if i < len (fullPlans ) {
120136 fullPlan = fullPlans [i ]
121137 }
122- if err := writeBaselineFile (baseline .Query , baselinePath , baseline .Plan , fullPlan ); err != nil {
138+ if err := writeBaselineFile (baseline .Query , baselinePath , baseline .Plan , fullPlan , useAnalyze ); err != nil {
123139 return err
124140 }
125141 }
126142
127143 return nil
128144}
129145
130- func writeBaselineFile (queryName , baselinePath string , filteredPlan map [string ]any , fullExplainPlan * ExplainOutput ) error {
146+ func writeBaselineFile (queryName , baselinePath string , filteredPlan map [string ]any , fullExplainPlan * ExplainOutput , useAnalyze bool ) error {
131147 var planSignature * PlanSignature
132148 if fullExplainPlan != nil {
133149 planSignature = ExtractPlanSignatureFromNode (& fullExplainPlan .Plan )
@@ -140,6 +156,20 @@ func writeBaselineFile(queryName, baselinePath string, filteredPlan map[string]a
140156 PlanSignature : planSignature ,
141157 }
142158
159+ if useAnalyze && fullExplainPlan != nil {
160+ baseline .AnalyzeMode = true
161+ baseline .Buffers = & BufferBaseline {
162+ SharedHitBlocks : fullExplainPlan .Plan .SharedHitBlocks ,
163+ SharedReadBlocks : fullExplainPlan .Plan .SharedReadBlocks ,
164+ TotalBuffers : fullExplainPlan .Plan .SharedHitBlocks + fullExplainPlan .Plan .SharedReadBlocks ,
165+ }
166+ baseline .Actuals = & ActualBaseline {
167+ ActualRows : fullExplainPlan .Plan .ActualRows ,
168+ PlanRows : fullExplainPlan .Plan .PlanRows ,
169+ ExecutionTimeMs : fullExplainPlan .ExecutionTime ,
170+ }
171+ }
172+
143173 jsonBytes , err := json .MarshalIndent (baseline , "" , " " )
144174 if err != nil {
145175 return fmt .Errorf ("failed to marshal baseline to JSON: %w" , err )
@@ -149,12 +179,15 @@ func writeBaselineFile(queryName, baselinePath string, filteredPlan map[string]a
149179 return fmt .Errorf ("failed to write baseline JSON: %w" , err )
150180 }
151181
152- fmt .Printf (" Created baseline: %s\n " , filepath .Base (baselinePath ))
182+ mode := ""
183+ if useAnalyze {
184+ mode = " [analyze]"
185+ }
186+ fmt .Printf (" Created baseline: %s%s\n " , filepath .Base (baselinePath ), mode )
153187 return nil
154188}
155189
156- // BaselineQueries creates baselines for all queries in the suite
157- func BaselineQueries (root string , runFilter string ) {
190+ func BaselineQueries (root string , runFilter string , analyzeOverride bool ) {
158191 config , err := ReadConfig (root )
159192 ignorePatterns := []string {}
160193 if err == nil {
@@ -168,6 +201,8 @@ func BaselineQueries(root string, runFilter string) {
168201 fmt .Printf ("Error reading config: %s\n " , err .Error ())
169202 os .Exit (3 )
170203 }
204+ SetGlobalConfig (config )
205+ useAnalyze := analyzeOverride || IsAnalyzeEnabled ()
171206
172207 if err := TestConnectionString (config .PgUri ); err != nil {
173208 fmt .Printf ("Error connecting to database: %s\n " , err .Error ())
@@ -189,7 +224,11 @@ func BaselineQueries(root string, runFilter string) {
189224 os .Exit (11 )
190225 }
191226
192- fmt .Println ("\n Creating baselines for queries:" )
227+ mode := "cost-based"
228+ if useAnalyze {
229+ mode = "analyze (buffers)"
230+ }
231+ fmt .Printf ("\n Creating baselines for queries (%s):\n " , mode )
193232
194233 for _ , folder := range suite .Dirs {
195234 folderBaselineDir := filepath .Join (baselineDir , folder .Dir )
@@ -227,7 +266,7 @@ func BaselineQueries(root string, runFilter string) {
227266 continue
228267 }
229268
230- if err := q .CreateBaseline (folderBaselineDir , folderPlanDir , db ); err != nil {
269+ if err := q .CreateBaseline (folderBaselineDir , folderPlanDir , db , useAnalyze ); err != nil {
231270 fmt .Printf (" Error creating baseline for %s: %s\n " , q .Name , err .Error ())
232271 }
233272 }
@@ -265,3 +304,14 @@ func CompareCost(actualCost, baselineCost, thresholdPercent float64) (bool, floa
265304
266305 return isOk , percentageIncrease
267306}
307+
308+ func CompareBuffers (actualBuffers , baselineBuffers int64 , thresholdPercent float64 ) (bool , float64 ) {
309+ if baselineBuffers == 0 {
310+ return actualBuffers == 0 , 0
311+ }
312+
313+ percentageIncrease := (float64 (actualBuffers - baselineBuffers ) / float64 (baselineBuffers )) * 100
314+ isOk := percentageIncrease <= thresholdPercent
315+
316+ return isOk , percentageIncrease
317+ }
0 commit comments