-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathpgexplain_pg18_test.go
More file actions
166 lines (153 loc) · 6.29 KB
/
Copy pathpgexplain_pg18_test.go
File metadata and controls
166 lines (153 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/*
2026 © Postgres.ai
*/
package pgexplain
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// PostgreSQL 18 reports fractional, two-decimal "Actual Rows" (averaged over
// loops) even for whole counts, e.g. "Actual Rows": 5.00 and 0.50. Before the
// uint64->float64 fix, json.Decode rejects the decimal and NewExplain fails for
// every PG18 EXPLAIN ANALYZE. This fixture reproduces a nested loop whose inner
// node averages to 0.50 rows/loop.
const inputJSONPostgres18FractionalRows = `[
{
"Plan": {
"Node Type": "Nested Loop",
"Parallel Aware": false,
"Join Type": "Inner",
"Startup Cost": 0.15,
"Total Cost": 12.30,
"Plan Rows": 5,
"Plan Width": 8,
"Actual Startup Time": 0.020,
"Actual Total Time": 0.030,
"Actual Rows": 5.00,
"Actual Loops": 1,
"Disabled": false,
"Plans": [
{
"Node Type": "Seq Scan",
"Parent Relationship": "Outer",
"Parallel Aware": false,
"Relation Name": "p",
"Alias": "p",
"Startup Cost": 0.00,
"Total Cost": 1.10,
"Plan Rows": 10,
"Plan Width": 4,
"Actual Startup Time": 0.005,
"Actual Total Time": 0.007,
"Actual Rows": 10.00,
"Actual Loops": 1,
"Disabled": false
},
{
"Node Type": "Index Only Scan",
"Parent Relationship": "Inner",
"Parallel Aware": false,
"Scan Direction": "Forward",
"Index Name": "c_pid_idx",
"Relation Name": "c",
"Alias": "c",
"Startup Cost": 0.00,
"Total Cost": 1.12,
"Plan Rows": 1,
"Plan Width": 4,
"Actual Startup Time": 0.001,
"Actual Total Time": 0.001,
"Actual Rows": 0.50,
"Actual Loops": 10,
"Index Cond": "(pid = p.id)",
"Heap Fetches": 0,
"Index Searches": 1,
"Disabled": false
}
]
},
"Planning Time": 0.100,
"Triggers": [],
"Execution Time": 0.050
}
]`
func TestRenderPostgres18FractionalRows(t *testing.T) {
explain, err := NewExplain(inputJSONPostgres18FractionalRows)
require.NoError(t, err) // before fix: cannot unmarshal number 5.00 into Go ... uint64
out := explain.RenderPlanText()
// Whole counts render as integers (back-compat with pre-18 output);
// fractional averages render with two decimals (matching PG18).
require.Contains(t, out, "rows=5 loops=1", "whole actual rows should render as an integer")
require.Contains(t, out, "rows=10 loops=1", "whole actual rows should render as an integer")
require.Contains(t, out, "rows=0.50 loops=10", "fractional actual rows should render with two decimals")
require.NotContains(t, out, "rows=5.00", "whole actual rows must not gain a decimal suffix")
}
// inputJSONPostgres18DetailFields carries PG18 per-node additions: Disabled (only
// the Seq Scan is disabled), Index Searches, WAL Buffers Full, and Storage on a
// Materialize node.
const inputJSONPostgres18DetailFields = `[
{
"Plan": {
"Node Type": "Aggregate", "Strategy": "Plain", "Parallel Aware": false,
"Startup Cost": 10.00, "Total Cost": 10.01, "Plan Rows": 1, "Plan Width": 8,
"Actual Startup Time": 0.5, "Actual Total Time": 0.5, "Actual Rows": 1.00, "Actual Loops": 1,
"Disabled": false,
"WAL Records": 6, "WAL FPI": 0, "WAL Bytes": 369, "WAL Buffers Full": 2,
"Plans": [
{
"Node Type": "Materialize", "Parent Relationship": "Outer", "Parallel Aware": false,
"Startup Cost": 0.0, "Total Cost": 1.0, "Plan Rows": 5, "Plan Width": 4,
"Actual Startup Time": 0.1, "Actual Total Time": 0.2, "Actual Rows": 5.00, "Actual Loops": 1,
"Storage": "Memory", "Maximum Storage": 17,
"Plans": [
{
"Node Type": "Seq Scan", "Parent Relationship": "Outer", "Parallel Aware": false,
"Relation Name": "noidx", "Alias": "noidx",
"Startup Cost": 0.0, "Total Cost": 1.0, "Plan Rows": 5, "Plan Width": 4,
"Actual Startup Time": 0.01, "Actual Total Time": 0.02, "Actual Rows": 5.00, "Actual Loops": 1,
"Disabled": true
}
]
},
{
"Node Type": "Index Only Scan", "Parent Relationship": "Inner", "Parallel Aware": false,
"Scan Direction": "Forward", "Index Name": "c_pid_idx", "Relation Name": "c", "Alias": "c",
"Startup Cost": 0.0, "Total Cost": 1.0, "Plan Rows": 1, "Plan Width": 4,
"Actual Startup Time": 0.001, "Actual Total Time": 0.001, "Actual Rows": 1.00, "Actual Loops": 1,
"Heap Fetches": 0, "Index Searches": 3
}
]
},
"Planning Time": 0.1, "Triggers": [], "Execution Time": 0.6
}
]`
// TestNegativeQueryIdentifier guards against the uint64 query-id bug: PostgreSQL's
// query identifier is a signed 64-bit value (often negative) and is emitted when
// compute_query_id is active (e.g. pg_stat_statements loaded). Decoding it into a
// uint64 fails for negative values, breaking the whole EXPLAIN.
func TestNegativeQueryIdentifier(t *testing.T) {
const j = `[{
"Plan": {
"Node Type": "Result", "Parallel Aware": false,
"Startup Cost": 0.0, "Total Cost": 0.01, "Plan Rows": 1, "Plan Width": 4,
"Actual Startup Time": 0.0, "Actual Total Time": 0.0, "Actual Rows": 1.00, "Actual Loops": 1
},
"Query Identifier": -4586892858755505326,
"Planning Time": 0.1, "Triggers": [], "Execution Time": 0.1
}]`
explain, err := NewExplain(j)
require.NoError(t, err) // before fix: cannot unmarshal number -4586892858755505326 into uint64
require.Equal(t, int64(-4586892858755505326), explain.QueryIdentifier)
require.Contains(t, explain.RenderPlanText(), "Query ID: -4586892858755505326")
}
func TestRenderPostgres18DetailFields(t *testing.T) {
explain, err := NewExplain(inputJSONPostgres18DetailFields)
require.NoError(t, err)
out := explain.RenderPlanText()
require.Contains(t, out, "Disabled: true", "the disabled Seq Scan should be annotated")
require.Equal(t, 1, strings.Count(out, "Disabled: true"), "only genuinely disabled nodes are annotated")
require.Contains(t, out, "Index Searches: 3")
require.Contains(t, out, "Storage: Memory Maximum Storage: 17kB")
require.Contains(t, out, "WAL: records=6 fpi=0 bytes=369 buffers-full=2")
}