-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmain.go
More file actions
132 lines (103 loc) · 3.89 KB
/
Copy pathmain.go
File metadata and controls
132 lines (103 loc) · 3.89 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
/*
2026 © Postgres.ai
*/
// Command explainrender converts a PostgreSQL `EXPLAIN (FORMAT JSON)` document
// into PostgreSQL's standard text plan (and optionally a stats summary) using
// joe's pkg/pgexplain renderer. joe collects plans as JSON but often needs to
// show the familiar text plan, and neither psql nor PostgreSQL can convert an
// existing JSON plan back to text — so pkg/pgexplain performs that translation.
//
// It is handy for debugging that JSON->text translation and for diffing joe's
// output against PostgreSQL's own text EXPLAIN across server versions; any
// difference is a rendering-fidelity bug.
//
// Usage:
//
// explainrender [-stats] [file]
//
// The input (a file path argument, or stdin when omitted) must be the output of
// EXPLAIN (... FORMAT JSON), e.g. EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ....
package main
import (
"flag"
"fmt"
"io"
"os"
"gitlab.com/postgres-ai/joe/pkg/pgexplain"
)
const usage = `explainrender converts a PostgreSQL EXPLAIN (FORMAT JSON) document into
PostgreSQL's standard text plan (and optionally a stats summary) using joe's
pkg/pgexplain renderer. psql cannot convert an existing JSON plan back to text;
joe receives plans as JSON, so pkg/pgexplain re-renders the standard text form.
Useful for debugging that JSON->text translation and for diffing joe's output
against PostgreSQL's own text EXPLAIN across versions (any difference is a bug).
Usage:
explainrender [-stats] [file]
The input must be the output of EXPLAIN (... FORMAT JSON), for example:
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT ...
With no file argument, the JSON is read from stdin.
Flags:
`
// statsSeparator delimits the rendered plan from the rendered stats summary in
// the combined output produced with -stats.
const statsSeparator = "\n===== STATS =====\n"
// render reads an EXPLAIN (FORMAT JSON) document from in, renders it with joe's
// pgexplain renderer, and writes the text plan to out. When withStats is true it
// also appends the stats summary under statsSeparator.
func render(in io.Reader, out io.Writer, withStats bool) error {
data, err := io.ReadAll(in)
if err != nil {
return fmt.Errorf("failed to read input: %w", err)
}
ex, err := pgexplain.NewExplain(string(data))
if err != nil {
return fmt.Errorf("failed to parse EXPLAIN JSON: %w", err)
}
if _, err := io.WriteString(out, ex.RenderPlanText()); err != nil {
return fmt.Errorf("failed to write plan: %w", err)
}
if withStats {
if _, err := io.WriteString(out, statsSeparator); err != nil {
return fmt.Errorf("failed to write stats separator: %w", err)
}
if _, err := io.WriteString(out, ex.RenderStats()); err != nil {
return fmt.Errorf("failed to write stats: %w", err)
}
}
return nil
}
// run resolves the input source (the given file path, or stdin when path is
// empty) and hands it to render. It is split out from main so that a deferred
// file close runs before main decides on the process exit code.
func run(path string, out io.Writer, withStats bool) error {
var in io.Reader = os.Stdin
if path != "" {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open %s: %w", path, err)
}
defer func() { _ = f.Close() }()
in = f
}
return render(in, out, withStats)
}
func main() {
flag.Usage = func() {
_, _ = fmt.Fprint(flag.CommandLine.Output(), usage)
flag.PrintDefaults()
}
withStats := flag.Bool("stats", false, "also render joe's stats summary after the plan")
flag.Parse()
// flag stops parsing at the first non-flag argument, so a flag placed after
// the file path (e.g. `explainrender file.json -stats`) would be silently
// swallowed as a second positional. Reject extra arguments instead so the
// misuse surfaces rather than quietly dropping the flag.
if flag.NArg() > 1 {
flag.Usage()
os.Exit(1)
}
if err := run(flag.Arg(0), os.Stdout, *withStats); err != nil {
_, _ = fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}