├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── example.json ├── go.mod ├── go.sum ├── main.go ├── out └── .gitignore ├── pev ├── go.mod ├── go.sum └── visualize.go └── pybindings ├── go.mod ├── go.sum ├── pycmdpev.go └── python3.go /.gitignore: -------------------------------------------------------------------------------- 1 | pycmdpev.so 2 | pycmdpev.h 3 | gocmdpev 4 | releases 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Simon Engledew 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | out/gocmdpev: go.mod go.sum main.go $(wildcard pev/*) 2 | go build -trimpath -o $(abspath $@) . 3 | 4 | out/pycmdpev.so: $(wildcard pybindings/*) 5 | (cd pybindings && go build -buildmode=c-shared -o $(abspath $@) .) 6 | 7 | .PHONY: python3 8 | python3: out/pycmdpev.so 9 | pkg-config --exists python3 && (cd out; python3 -c 'import pycmdpev, sys; pycmdpev.visualize(open(sys.argv[1]).read())' $(abspath example.json)) 10 | 11 | .PHONY: python3-docker 12 | python3-docker: 13 | echo 'FROM golang:1.14-buster\nRUN apt-get update && apt-get install --yes --no-install-recommends python3-dev' | docker build -t gocmdpev-python3 - 14 | docker run -v "$(abspath .):/workspace" -w /workspace -it --rm gocmdpev-python3 make out/pycmdpev.so 15 | 16 | .PHONY: test 17 | test: out/gocmdpev 18 | cat example.json | out/gocmdpev 19 | 20 | .PHONY: install 21 | install: 22 | go install . 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gocmdpev 2 | A command-line GO Postgres query visualizer, heavily inspired by the excellent (web-based) [pev](https://github.com/AlexTatiyants/pev) 3 | 4 | ![image](https://cloud.githubusercontent.com/assets/14410/15449922/bd129a10-1f83-11e6-9480-b4c103d7c0a5.png) 5 | 6 | ## Usage 7 | 8 | ``` 9 | go get -u github.com/simon-engledew/gocmdpev 10 | ``` 11 | 12 | or via Homebrew: 13 | 14 | ``` 15 | brew tap simon-engledew/gocmdpev 16 | brew install gocmdpev 17 | ``` 18 | 19 | Generate a query plan with all the trimmings by prefixing your query with: 20 | 21 | ```pgsql 22 | EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) 23 | ``` 24 | 25 | Then pipe the resulting query plan into `gocmdpev`. 26 | 27 | On MacOS you can just grab a query on your clipboard and run this one-liner: 28 | 29 | ```bash 30 | pbpaste | sed '1s/^/EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) /' | psql -qXAt | gocmdpev 31 | ``` 32 | 33 | ## Python 3 Bindings 34 | 35 | Check out the repository and run `make python3` to build and test the bindings. 36 | 37 | ## Using with Ruby on Rails 38 | 39 | Try the [`pg-eyeballs`](https://github.com/bradurani/pg-eyeballs) gem 40 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Plan": { 4 | "Node Type": "Aggregate", 5 | "Strategy": "Plain", 6 | "Partial Mode": "Simple", 7 | "Parallel Aware": false, 8 | "Startup Cost": 251.59, 9 | "Total Cost": 251.61, 10 | "Plan Rows": 1, 11 | "Plan Width": 80, 12 | "Actual Startup Time": 18.166, 13 | "Actual Total Time": 18.166, 14 | "Actual Rows": 1, 15 | "Actual Loops": 1, 16 | "Output": ["$3", "count(ROW(pg_source.uuid, pg_source.task_state, pg_source.exit_code, pg_source.stopped_at, pg_source.started_at, pg_source.\"user\", pg_source.image))", "'{}'::text[]", "(COALESCE(json_agg(ROW(pg_source.uuid, pg_source.task_state, pg_source.exit_code, pg_source.stopped_at, pg_source.started_at, pg_source.\"user\", pg_source.image)), '[]'::json))::character varying"], 17 | "Shared Hit Blocks": 2537, 18 | "Shared Read Blocks": 0, 19 | "Shared Dirtied Blocks": 0, 20 | "Shared Written Blocks": 0, 21 | "Local Hit Blocks": 0, 22 | "Local Read Blocks": 0, 23 | "Local Dirtied Blocks": 0, 24 | "Local Written Blocks": 0, 25 | "Temp Read Blocks": 0, 26 | "Temp Written Blocks": 0, 27 | "Plans": [ 28 | { 29 | "Node Type": "Limit", 30 | "Parent Relationship": "InitPlan", 31 | "Subplan Name": "CTE pg_source", 32 | "Parallel Aware": false, 33 | "Startup Cost": 0.55, 34 | "Total Cost": 46.74, 35 | "Plan Rows": 51, 36 | "Plan Width": 108, 37 | "Actual Startup Time": 0.738, 38 | "Actual Total Time": 3.968, 39 | "Actual Rows": 51, 40 | "Actual Loops": 1, 41 | "Output": ["tasks.uuid", "(CASE WHEN tasks.defunct THEN 'defunct'::task_state WHEN (tasks.cancelled_at IS NOT NULL) THEN 'cancelled'::task_state WHEN (tasks.stopped_at IS NOT NULL) THEN CASE tasks.exit_code WHEN 0 THEN 'success'::task_state ELSE 'failed'::task_state END ELSE 'running'::task_state END)", "tasks.exit_code", "tasks.stopped_at", "tasks.started_at", "(row_to_json((ROW(users.username))))", "(row_to_json((ROW(images.id, images.name, images.description, images.cost, images.hash, images.author, images.tags, images.owner_id, images.deleted_at, images.updated_at, images.icon))))", "tasks.id"], 42 | "Shared Hit Blocks": 371, 43 | "Shared Read Blocks": 0, 44 | "Shared Dirtied Blocks": 0, 45 | "Shared Written Blocks": 0, 46 | "Local Hit Blocks": 0, 47 | "Local Read Blocks": 0, 48 | "Local Dirtied Blocks": 0, 49 | "Local Written Blocks": 0, 50 | "Temp Read Blocks": 0, 51 | "Temp Written Blocks": 0, 52 | "Plans": [ 53 | { 54 | "Node Type": "Nested Loop", 55 | "Parent Relationship": "Outer", 56 | "Parallel Aware": false, 57 | "Join Type": "Left", 58 | "Startup Cost": 0.55, 59 | "Total Cost": 430.75, 60 | "Plan Rows": 475, 61 | "Plan Width": 108, 62 | "Actual Startup Time": 0.736, 63 | "Actual Total Time": 3.945, 64 | "Actual Rows": 51, 65 | "Actual Loops": 1, 66 | "Output": ["tasks.uuid", "CASE WHEN tasks.defunct THEN 'defunct'::task_state WHEN (tasks.cancelled_at IS NOT NULL) THEN 'cancelled'::task_state WHEN (tasks.stopped_at IS NOT NULL) THEN CASE tasks.exit_code WHEN 0 THEN 'success'::task_state ELSE 'failed'::task_state END ELSE 'running'::task_state END", "tasks.exit_code", "tasks.stopped_at", "tasks.started_at", "row_to_json((ROW(users.username)))", "row_to_json((ROW(images.id, images.name, images.description, images.cost, images.hash, images.author, images.tags, images.owner_id, images.deleted_at, images.updated_at, images.icon)))", "tasks.id"], 67 | "Inner Unique": true, 68 | "Shared Hit Blocks": 371, 69 | "Shared Read Blocks": 0, 70 | "Shared Dirtied Blocks": 0, 71 | "Shared Written Blocks": 0, 72 | "Local Hit Blocks": 0, 73 | "Local Read Blocks": 0, 74 | "Local Dirtied Blocks": 0, 75 | "Local Written Blocks": 0, 76 | "Temp Read Blocks": 0, 77 | "Temp Written Blocks": 0, 78 | "Plans": [ 79 | { 80 | "Node Type": "Nested Loop", 81 | "Parent Relationship": "Outer", 82 | "Parallel Aware": false, 83 | "Join Type": "Left", 84 | "Startup Cost": 0.41, 85 | "Total Cost": 343.75, 86 | "Plan Rows": 475, 87 | "Plan Width": 85, 88 | "Actual Startup Time": 0.605, 89 | "Actual Total Time": 2.267, 90 | "Actual Rows": 51, 91 | "Actual Loops": 1, 92 | "Output": ["tasks.uuid", "tasks.defunct", "tasks.cancelled_at", "tasks.stopped_at", "tasks.exit_code", "tasks.started_at", "tasks.id", "tasks.image_id", "(ROW(users.username))"], 93 | "Inner Unique": true, 94 | "Shared Hit Blocks": 265, 95 | "Shared Read Blocks": 0, 96 | "Shared Dirtied Blocks": 0, 97 | "Shared Written Blocks": 0, 98 | "Local Hit Blocks": 0, 99 | "Local Read Blocks": 0, 100 | "Local Dirtied Blocks": 0, 101 | "Local Written Blocks": 0, 102 | "Temp Read Blocks": 0, 103 | "Temp Written Blocks": 0, 104 | "Plans": [ 105 | { 106 | "Node Type": "Index Scan", 107 | "Parent Relationship": "Outer", 108 | "Parallel Aware": false, 109 | "Scan Direction": "Backward", 110 | "Index Name": "tasks_pkey", 111 | "Relation Name": "tasks", 112 | "Schema": "public", 113 | "Alias": "tasks", 114 | "Startup Cost": 0.28, 115 | "Total Cost": 258.71, 116 | "Plan Rows": 475, 117 | "Plan Width": 57, 118 | "Actual Startup Time": 0.593, 119 | "Actual Total Time": 2.013, 120 | "Actual Rows": 51, 121 | "Actual Loops": 1, 122 | "Output": ["tasks.id", "tasks.image_id", "tasks.container_id", "tasks.uuid", "tasks.user_id", "tasks.exit_code", "tasks.defunct", "tasks.started_at", "tasks.stopped_at", "tasks.cancelled_at", "tasks.cancelled_by"], 123 | "Filter": "(is_running(tasks.*) IS FALSE)", 124 | "Rows Removed by Filter": 2, 125 | "Shared Hit Blocks": 163, 126 | "Shared Read Blocks": 0, 127 | "Shared Dirtied Blocks": 0, 128 | "Shared Written Blocks": 0, 129 | "Local Hit Blocks": 0, 130 | "Local Read Blocks": 0, 131 | "Local Dirtied Blocks": 0, 132 | "Local Written Blocks": 0, 133 | "Temp Read Blocks": 0, 134 | "Temp Written Blocks": 0 135 | }, 136 | { 137 | "Node Type": "Index Scan", 138 | "Parent Relationship": "Inner", 139 | "Parallel Aware": false, 140 | "Scan Direction": "Forward", 141 | "Index Name": "users_pkey", 142 | "Relation Name": "users", 143 | "Schema": "public", 144 | "Alias": "users", 145 | "Startup Cost": 0.14, 146 | "Total Cost": 0.18, 147 | "Plan Rows": 1, 148 | "Plan Width": 36, 149 | "Actual Startup Time": 0.003, 150 | "Actual Total Time": 0.003, 151 | "Actual Rows": 1, 152 | "Actual Loops": 51, 153 | "Output": ["users.id", "ROW(users.username)"], 154 | "Index Cond": "(users.id = tasks.user_id)", 155 | "Rows Removed by Index Recheck": 0, 156 | "Shared Hit Blocks": 102, 157 | "Shared Read Blocks": 0, 158 | "Shared Dirtied Blocks": 0, 159 | "Shared Written Blocks": 0, 160 | "Local Hit Blocks": 0, 161 | "Local Read Blocks": 0, 162 | "Local Dirtied Blocks": 0, 163 | "Local Written Blocks": 0, 164 | "Temp Read Blocks": 0, 165 | "Temp Written Blocks": 0 166 | } 167 | ] 168 | }, 169 | { 170 | "Node Type": "Index Scan", 171 | "Parent Relationship": "Inner", 172 | "Parallel Aware": false, 173 | "Scan Direction": "Forward", 174 | "Index Name": "images_pkey", 175 | "Relation Name": "images", 176 | "Schema": "public", 177 | "Alias": "images", 178 | "Startup Cost": 0.14, 179 | "Total Cost": 0.18, 180 | "Plan Rows": 1, 181 | "Plan Width": 36, 182 | "Actual Startup Time": 0.004, 183 | "Actual Total Time": 0.004, 184 | "Actual Rows": 1, 185 | "Actual Loops": 51, 186 | "Output": ["images.id", "ROW(images.id, images.name, images.description, images.cost, images.hash, images.author, images.tags, images.owner_id, images.deleted_at, images.updated_at, images.icon)"], 187 | "Index Cond": "(images.id = tasks.image_id)", 188 | "Rows Removed by Index Recheck": 0, 189 | "Shared Hit Blocks": 102, 190 | "Shared Read Blocks": 0, 191 | "Shared Dirtied Blocks": 0, 192 | "Shared Written Blocks": 0, 193 | "Local Hit Blocks": 0, 194 | "Local Read Blocks": 0, 195 | "Local Dirtied Blocks": 0, 196 | "Local Written Blocks": 0, 197 | "Temp Read Blocks": 0, 198 | "Temp Written Blocks": 0 199 | } 200 | ] 201 | } 202 | ] 203 | }, 204 | { 205 | "Node Type": "Aggregate", 206 | "Strategy": "Plain", 207 | "Partial Mode": "Simple", 208 | "Parent Relationship": "InitPlan", 209 | "Subplan Name": "InitPlan 2 (returns $3)", 210 | "Parallel Aware": false, 211 | "Startup Cost": 203.57, 212 | "Total Cost": 203.58, 213 | "Plan Rows": 1, 214 | "Plan Width": 8, 215 | "Actual Startup Time": 12.861, 216 | "Actual Total Time": 12.862, 217 | "Actual Rows": 1, 218 | "Actual Loops": 1, 219 | "Output": ["count(*)"], 220 | "Shared Hit Blocks": 2158, 221 | "Shared Read Blocks": 0, 222 | "Shared Dirtied Blocks": 0, 223 | "Shared Written Blocks": 0, 224 | "Local Hit Blocks": 0, 225 | "Local Read Blocks": 0, 226 | "Local Dirtied Blocks": 0, 227 | "Local Written Blocks": 0, 228 | "Temp Read Blocks": 0, 229 | "Temp Written Blocks": 0, 230 | "Plans": [ 231 | { 232 | "Node Type": "Seq Scan", 233 | "Parent Relationship": "Outer", 234 | "Parallel Aware": false, 235 | "Relation Name": "tasks", 236 | "Schema": "public", 237 | "Alias": "tasks_1", 238 | "Startup Cost": 0.00, 239 | "Total Cost": 202.38, 240 | "Plan Rows": 475, 241 | "Plan Width": 0, 242 | "Actual Startup Time": 0.505, 243 | "Actual Total Time": 12.615, 244 | "Actual Rows": 711, 245 | "Actual Loops": 1, 246 | "Output": ["tasks_1.id", "tasks_1.image_id", "tasks_1.container_id", "tasks_1.uuid", "tasks_1.user_id", "tasks_1.exit_code", "tasks_1.defunct", "tasks_1.started_at", "tasks_1.stopped_at", "tasks_1.cancelled_at", "tasks_1.cancelled_by"], 247 | "Filter": "(is_running(tasks_1.*) IS FALSE)", 248 | "Rows Removed by Filter": 2, 249 | "Shared Hit Blocks": 2158, 250 | "Shared Read Blocks": 0, 251 | "Shared Dirtied Blocks": 0, 252 | "Shared Written Blocks": 0, 253 | "Local Hit Blocks": 0, 254 | "Local Read Blocks": 0, 255 | "Local Dirtied Blocks": 0, 256 | "Local Written Blocks": 0, 257 | "Temp Read Blocks": 0, 258 | "Temp Written Blocks": 0 259 | } 260 | ] 261 | }, 262 | { 263 | "Node Type": "CTE Scan", 264 | "Parent Relationship": "Outer", 265 | "Parallel Aware": false, 266 | "CTE Name": "pg_source", 267 | "Alias": "pg_source", 268 | "Startup Cost": 0.00, 269 | "Total Cost": 1.02, 270 | "Plan Rows": 51, 271 | "Plan Width": 104, 272 | "Actual Startup Time": 0.759, 273 | "Actual Total Time": 4.202, 274 | "Actual Rows": 51, 275 | "Actual Loops": 1, 276 | "Output": ["pg_source.uuid", "pg_source.task_state", "pg_source.exit_code", "pg_source.stopped_at", "pg_source.started_at", "pg_source.\"user\"", "pg_source.image"], 277 | "Shared Hit Blocks": 371, 278 | "Shared Read Blocks": 0, 279 | "Shared Dirtied Blocks": 0, 280 | "Shared Written Blocks": 0, 281 | "Local Hit Blocks": 0, 282 | "Local Read Blocks": 0, 283 | "Local Dirtied Blocks": 0, 284 | "Local Written Blocks": 0, 285 | "Temp Read Blocks": 0, 286 | "Temp Written Blocks": 0 287 | } 288 | ] 289 | }, 290 | "Planning Time": 4.029, 291 | "Triggers": [ 292 | ], 293 | "Execution Time": 18.795 294 | } 295 | ] 296 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/simon-engledew/gocmdpev 2 | 3 | go 1.14 4 | 5 | replace github.com/simon-engledew/gocmdpev/pev => ./pev 6 | 7 | require ( 8 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 9 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 10 | github.com/fatih/color v1.9.0 11 | github.com/mattn/go-colorable v0.1.6 // indirect 12 | github.com/simon-engledew/gocmdpev/pev v0.0.0-20190513135149-ceda29aa18fc 13 | golang.org/x/sys v0.0.0-20200316230553-a7d97aace0b0 // indirect 14 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= 4 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 8 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 9 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 10 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 11 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 12 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 13 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 14 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 15 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 16 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 17 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 18 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 19 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= 20 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 21 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 22 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 23 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 24 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 25 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 26 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 30 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 31 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 32 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 33 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20200316230553-a7d97aace0b0 h1:4Khi5GeNOkZS5DqSBRn4Sy7BE6GuxwOqARPqfurkdNk= 37 | golang.org/x/sys v0.0.0-20200316230553-a7d97aace0b0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 39 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 43 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main // import "github.com/simon-engledew/gocmdpev" 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | "github.com/simon-engledew/gocmdpev/pev" 9 | "gopkg.in/alecthomas/kingpin.v2" 10 | ) 11 | 12 | var ( 13 | app = kingpin.New("gocmdpev", "A command-line GO Postgres query visualizer (see https://github.com/simon-engledew/gocmdpev).") 14 | ) 15 | 16 | func main() { 17 | app.HelpFlag.Short('h') 18 | app.Version("1.0.0") 19 | app.VersionFlag.Short('v') 20 | app.Parse(os.Args[1:]) 21 | 22 | err := pev.Visualize(color.Output, os.Stdin, 60) 23 | 24 | if err != nil { 25 | log.Fatalf("%v", err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /out/.gitignore: -------------------------------------------------------------------------------- 1 | pycmdpev.* 2 | -------------------------------------------------------------------------------- /pev/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/simon-engledew/gocmdpev/pev 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/dustin/go-humanize v1.0.0 7 | github.com/fatih/color v1.9.0 8 | github.com/mattn/go-colorable v0.1.6 // indirect 9 | github.com/mitchellh/go-wordwrap v1.0.0 10 | golang.org/x/sys v0.0.0-20200316230553-a7d97aace0b0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /pev/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 2 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 3 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 4 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 5 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 6 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 7 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 8 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 9 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 10 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 11 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 12 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 13 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 14 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= 15 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/sys v0.0.0-20200316230553-a7d97aace0b0 h1:4Khi5GeNOkZS5DqSBRn4Sy7BE6GuxwOqARPqfurkdNk= 20 | golang.org/x/sys v0.0.0-20200316230553-a7d97aace0b0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 21 | -------------------------------------------------------------------------------- /pev/visualize.go: -------------------------------------------------------------------------------- 1 | package pev // import "github.com/simon-engledew/gocmdpev/pev" 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | humanize "github.com/dustin/go-humanize" 10 | "github.com/fatih/color" 11 | wordwrap "github.com/mitchellh/go-wordwrap" 12 | ) 13 | 14 | type EstimateDirection string 15 | 16 | const ( 17 | Over EstimateDirection = "Over" 18 | Under = "Under" 19 | ) 20 | 21 | type NodeType string 22 | 23 | const ( 24 | Limit NodeType = "Limit" 25 | Append = "Append" 26 | Sort = "Sort" 27 | NestedLoop = "Nested Loop" 28 | MergeJoin = "Merge Join" 29 | Hash = "Hash" 30 | HashJoin = "Hash Join" 31 | Aggregate = "Aggregate" 32 | Hashaggregate = "Hashaggregate" 33 | SequenceScan = "Seq Scan" 34 | IndexScan = "Index Scan" 35 | IndexOnlyScan = "Index Only Scan" 36 | BitmapHeapScan = "Bitmap Heap Scan" 37 | BitmapIndexScan = "Bitmap Index Scan" 38 | CTEScan = "CTE Scan" 39 | ) 40 | 41 | var prefixFormat = color.New(color.FgHiBlack).SprintFunc() 42 | var tagFormat = color.New(color.FgWhite, color.BgRed).SprintFunc() 43 | var mutedFormat = color.New(color.FgHiBlack).SprintFunc() 44 | var boldFormat = color.New(color.FgHiWhite).SprintFunc() 45 | var goodFormat = color.New(color.FgGreen).SprintFunc() 46 | var warningFormat = color.New(color.FgHiYellow).SprintFunc() 47 | var criticalFormat = color.New(color.FgHiRed).SprintFunc() 48 | var outputFormat = color.New(color.FgCyan).SprintFunc() 49 | 50 | var Descriptions = map[NodeType]string{ 51 | Append: "Used in a UNION to merge multiple record sets by appending them together.", 52 | Limit: "Returns a specified number of rows from a record set.", 53 | Sort: "Sorts a record set based on the specified sort key.", 54 | NestedLoop: "Merges two record sets by looping through every record in the first set and trying to find a match in the second set. All matching records are returned.", 55 | MergeJoin: "Merges two record sets by first sorting them on a join key.", 56 | Hash: "Generates a hash table from the records in the input recordset. Hash is used by Hash Join.", 57 | HashJoin: "Joins to record sets by hashing one of them (using a Hash Scan).", 58 | Aggregate: "Groups records together based on a GROUP BY or aggregate function (e.g. sum()).", 59 | Hashaggregate: "Groups records together based on a GROUP BY or aggregate function (e.g. sum()). Hash Aggregate uses a hash to first organize the records by a key.", 60 | SequenceScan: "Finds relevant records by sequentially scanning the input record set. When reading from a table, Seq Scans (unlike Index Scans) perform a single read operation (only the table is read).", 61 | IndexScan: "Finds relevant records based on an Index. Index Scans perform 2 read operations: one to read the index and another to read the actual value from the table.", 62 | IndexOnlyScan: "Finds relevant records based on an Index. Index Only Scans perform a single read operation from the index and do not read from the corresponding table.", 63 | BitmapHeapScan: "Searches through the pages returned by the Bitmap Index Scan for relevant rows.", 64 | BitmapIndexScan: "Uses a Bitmap Index (index which uses 1 bit per page) to find all relevant pages. Results of this node are fed to the Bitmap Heap Scan.", 65 | CTEScan: "Performs a sequential scan of Common Table Expression (CTE) query results. Note that results of a CTE are materialized (calculated and temporarily stored).", 66 | } 67 | 68 | type Explain struct { 69 | Plan Plan `json:"Plan"` 70 | PlanningTime float64 `json:"Planning Time"` 71 | Triggers []interface{} `json:"Triggers"` 72 | ExecutionTime float64 `json:"Execution Time"` 73 | TotalCost float64 74 | MaxRows uint64 75 | MaxCost float64 76 | MaxDuration float64 77 | } 78 | 79 | type Plan struct { 80 | ActualCost float64 81 | ActualDuration float64 82 | ActualLoops uint64 `json:"Actual Loops"` 83 | ActualRows uint64 `json:"Actual Rows"` 84 | ActualStartupTime float64 `json:"Actual Startup Time"` 85 | ActualTotalTime float64 `json:"Actual Total Time"` 86 | Alias string `json:"Alias"` 87 | Costliest bool 88 | CTEName string `json:"CTE Name"` 89 | Filter string `json:"Filter"` 90 | GroupKey []string `json:"Group Key"` 91 | HashCondition string `json:"Hash Cond"` 92 | HeapFetches uint64 `json:"Heap Fetches"` 93 | IndexCondition string `json:"Index Cond"` 94 | IndexName string `json:"Index Name"` 95 | IOReadTime float64 `json:"I/O Read Time"` 96 | IOWriteTime float64 `json:"I/O Write Time"` 97 | JoinType string `json:"Join Type"` 98 | Largest bool 99 | LocalDirtiedBlocks uint64 `json:"Local Dirtied Blocks"` 100 | LocalHitBlocks uint64 `json:"Local Hit Blocks"` 101 | LocalReadBlocks uint64 `json:"Local Read Blocks"` 102 | LocalWrittenBlocks uint64 `json:"Local Written Blocks"` 103 | NodeType NodeType `json:"Node Type"` 104 | Output []string `json:"Output"` 105 | ParentRelationship string `json:"Parent Relationship"` 106 | PlannerRowEstimateDirection EstimateDirection 107 | PlannerRowEstimateFactor float64 108 | PlanRows uint64 `json:"Plan Rows"` 109 | PlanWidth uint64 `json:"Plan Width"` 110 | RelationName string `json:"Relation Name"` 111 | RowsRemovedByFilter uint64 `json:"Rows Removed by Filter"` 112 | RowsRemovedByIndexRecheck uint64 `json:"Rows Removed by Index Recheck"` 113 | ScanDirection string `json:"Scan Direction"` 114 | Schema string `json:"Schema"` 115 | SharedDirtiedBlocks uint64 `json:"Shared Dirtied Blocks"` 116 | SharedHitBlocks uint64 `json:"Shared Hit Blocks"` 117 | SharedReadBlocks uint64 `json:"Shared Read Blocks"` 118 | SharedWrittenBlocks uint64 `json:"Shared Written Blocks"` 119 | Slowest bool 120 | StartupCost float64 `json:"Startup Cost"` 121 | Strategy string `json:"Strategy"` 122 | TempReadBlocks uint64 `json:"Temp Read Blocks"` 123 | TempWrittenBlocks uint64 `json:"Temp Written Blocks"` 124 | TotalCost float64 `json:"Total Cost"` 125 | Plans []Plan `json:"Plans"` 126 | } 127 | 128 | func calculatePlannerEstimate(explain *Explain, plan *Plan) { 129 | plan.PlannerRowEstimateFactor = 0 130 | 131 | if plan.PlanRows == plan.ActualRows { 132 | return 133 | } 134 | 135 | plan.PlannerRowEstimateDirection = Under 136 | if plan.PlanRows != 0 { 137 | plan.PlannerRowEstimateFactor = float64(plan.ActualRows) / float64(plan.PlanRows) 138 | } 139 | 140 | if plan.PlannerRowEstimateFactor < 1.0 { 141 | plan.PlannerRowEstimateFactor = 0 142 | plan.PlannerRowEstimateDirection = Over 143 | if plan.ActualRows != 0 { 144 | plan.PlannerRowEstimateFactor = float64(plan.PlanRows) / float64(plan.ActualRows) 145 | } 146 | } 147 | } 148 | 149 | func calculateActuals(explain *Explain, plan *Plan) { 150 | plan.ActualDuration = plan.ActualTotalTime 151 | plan.ActualCost = plan.TotalCost 152 | 153 | for _, child := range plan.Plans { 154 | if child.NodeType != CTEScan { 155 | plan.ActualDuration = plan.ActualDuration - child.ActualTotalTime 156 | plan.ActualCost = plan.ActualCost - child.TotalCost 157 | } 158 | } 159 | 160 | if plan.ActualCost < 0 { 161 | plan.ActualCost = 0 162 | } 163 | 164 | explain.TotalCost = explain.TotalCost + plan.ActualCost 165 | 166 | plan.ActualDuration = plan.ActualDuration * float64(plan.ActualLoops) 167 | } 168 | 169 | func calculateOutlierNodes(explain *Explain, plan *Plan) { 170 | plan.Costliest = plan.ActualCost == explain.MaxCost 171 | plan.Largest = plan.ActualRows == explain.MaxRows 172 | plan.Slowest = plan.ActualDuration == explain.MaxDuration 173 | 174 | for index := range plan.Plans { 175 | calculateOutlierNodes(explain, &plan.Plans[index]) 176 | } 177 | } 178 | 179 | func calculateMaximums(explain *Explain, plan *Plan) { 180 | if explain.MaxRows < plan.ActualRows { 181 | explain.MaxRows = plan.ActualRows 182 | } 183 | if explain.MaxCost < plan.ActualCost { 184 | explain.MaxCost = plan.ActualCost 185 | } 186 | if explain.MaxDuration < plan.ActualDuration { 187 | explain.MaxDuration = plan.ActualDuration 188 | } 189 | } 190 | 191 | func durationToString(value float64) string { 192 | if value < 1 { 193 | return goodFormat("<1 ms") 194 | } else if value < 100 { 195 | return goodFormat(fmt.Sprintf("%.2f ms", value)) 196 | } else if value < 1000 { 197 | return warningFormat(fmt.Sprintf("%.2f ms", value)) 198 | } else if value < 60000 { 199 | return criticalFormat(fmt.Sprintf("%.2f s", value/2000.0)) 200 | } else { 201 | return criticalFormat(fmt.Sprintf("%.2f m", value/60000.0)) 202 | } 203 | } 204 | 205 | func processExplain(explain *Explain) { 206 | processPlan(explain, &explain.Plan) 207 | calculateOutlierNodes(explain, &explain.Plan) 208 | } 209 | 210 | func processPlan(explain *Explain, plan *Plan) { 211 | calculatePlannerEstimate(explain, plan) 212 | calculateActuals(explain, plan) 213 | calculateMaximums(explain, plan) 214 | 215 | for index := range plan.Plans { 216 | processPlan(explain, &plan.Plans[index]) 217 | } 218 | } 219 | 220 | func writeExplain(writer io.Writer, explain *Explain, width uint) { 221 | fmt.Fprintf(writer, "○ Total Cost: %s\n", humanize.Commaf(explain.TotalCost)) 222 | fmt.Fprintf(writer, "○ Planning Time: %s\n", durationToString(explain.PlanningTime)) 223 | fmt.Fprintf(writer, "○ Execution Time: %s\n", durationToString(explain.ExecutionTime)) 224 | fmt.Fprintf(writer, prefixFormat("┬\n")) 225 | 226 | writePlan(writer, explain, &explain.Plan, "", 0, width, len(explain.Plan.Plans) == 1) 227 | } 228 | 229 | func formatDetails(plan *Plan) string { 230 | var details []string 231 | 232 | if plan.ScanDirection != "" { 233 | details = append(details, plan.ScanDirection) 234 | } 235 | 236 | if plan.Strategy != "" { 237 | details = append(details, plan.Strategy) 238 | } 239 | 240 | if len(details) > 0 { 241 | return mutedFormat(fmt.Sprintf(" [%v]", strings.Join(details, ", "))) 242 | } 243 | 244 | return "" 245 | } 246 | 247 | func formatTag(tag string) string { 248 | return tagFormat(fmt.Sprintf(" %v ", tag)) 249 | } 250 | 251 | func formatTags(plan *Plan) string { 252 | var tags []string 253 | 254 | if plan.Slowest { 255 | tags = append(tags, formatTag("slowest")) 256 | } 257 | if plan.Costliest { 258 | tags = append(tags, formatTag("costliest")) 259 | } 260 | if plan.Largest { 261 | tags = append(tags, formatTag("largest")) 262 | } 263 | if plan.PlannerRowEstimateFactor >= 100 { 264 | tags = append(tags, formatTag("bad estimate")) 265 | } 266 | 267 | return strings.Join(tags, " ") 268 | } 269 | 270 | func getTerminator(index int, plan *Plan) string { 271 | if index == 0 { 272 | if len(plan.Plans) == 0 { 273 | return "⌡► " 274 | } else { 275 | return "├► " 276 | } 277 | } else { 278 | if len(plan.Plans) == 0 { 279 | return " " 280 | } else { 281 | return "│ " 282 | } 283 | } 284 | } 285 | 286 | func wrapString(line string, width uint) string { 287 | if width == 0 { 288 | return line 289 | } 290 | return wordwrap.WrapString(line, width) 291 | } 292 | 293 | func writePlan(writer io.Writer, explain *Explain, plan *Plan, prefix string, depth int, width uint, lastChild bool) { 294 | currentPrefix := prefix 295 | 296 | var outputFn = func(format string, a ...interface{}) (int, error) { 297 | return fmt.Fprintf(writer, fmt.Sprintf("%s%s\n", prefixFormat(currentPrefix), format), a...) 298 | } 299 | 300 | outputFn(prefixFormat("│")) 301 | 302 | joint := "├" 303 | if len(plan.Plans) > 1 || lastChild { 304 | joint = "└" 305 | } 306 | 307 | outputFn("%v %v%v %v", prefixFormat(joint+"─⌠"), boldFormat(plan.NodeType), formatDetails(plan), formatTags(plan)) 308 | 309 | if len(plan.Plans) > 1 || lastChild { 310 | prefix += " " 311 | } else { 312 | prefix += "│ " 313 | } 314 | 315 | currentPrefix = prefix + "│ " 316 | 317 | cols := width - uint(len(currentPrefix)) 318 | 319 | for _, line := range strings.Split(wrapString(Descriptions[plan.NodeType], cols), "\n") { 320 | outputFn("%v", mutedFormat(line)) 321 | } 322 | 323 | outputFn("○ %v %v (%.0f%%)", "Duration:", durationToString(plan.ActualDuration), (plan.ActualDuration/explain.ExecutionTime)*100) 324 | 325 | outputFn("○ %v %v (%.0f%%)", "Cost:", humanize.Commaf(plan.ActualCost), (plan.ActualCost/explain.TotalCost)*100) 326 | 327 | outputFn("○ %v %v", "Rows:", humanize.Comma(int64(plan.ActualRows))) 328 | 329 | currentPrefix = currentPrefix + " " 330 | 331 | if plan.JoinType != "" { 332 | outputFn("%v %v", plan.JoinType, mutedFormat("join")) 333 | } 334 | 335 | if plan.RelationName != "" { 336 | outputFn("%v %v.%v", mutedFormat("on"), plan.Schema, plan.RelationName) 337 | } 338 | 339 | if plan.IndexName != "" { 340 | outputFn("%v %v", mutedFormat("using"), plan.IndexName) 341 | } 342 | 343 | if plan.IndexCondition != "" { 344 | outputFn("%v %v", mutedFormat("condition"), plan.IndexCondition) 345 | } 346 | 347 | if plan.Filter != "" { 348 | outputFn("%v %v %v", mutedFormat("filter"), plan.Filter, mutedFormat(fmt.Sprintf("[-%v rows]", humanize.Comma(int64(plan.RowsRemovedByFilter))))) 349 | } 350 | 351 | if plan.HashCondition != "" { 352 | outputFn("%v %v", mutedFormat("on"), plan.HashCondition) 353 | } 354 | 355 | if plan.CTEName != "" { 356 | outputFn("CTE %v", plan.CTEName) 357 | } 358 | 359 | if plan.PlannerRowEstimateFactor != 0 { 360 | outputFn("%v %vestimated %v %.2fx", mutedFormat("rows"), plan.PlannerRowEstimateDirection, mutedFormat("by"), plan.PlannerRowEstimateFactor) 361 | } 362 | 363 | currentPrefix = prefix 364 | 365 | if len(plan.Output) > 0 { 366 | for index, line := range strings.Split(wrapString(strings.Join(plan.Output, " + "), cols), "\n") { 367 | outputFn(prefixFormat(getTerminator(index, plan)) + outputFormat(line)) 368 | } 369 | } 370 | 371 | for index := range plan.Plans { 372 | writePlan(writer, explain, &plan.Plans[index], prefix, depth+1, width, index == len(plan.Plans)-1) 373 | } 374 | } 375 | 376 | func Visualize(writer io.Writer, reader io.Reader, width uint) error { 377 | var explain []Explain 378 | 379 | err := json.NewDecoder(reader).Decode(&explain) 380 | 381 | if err != nil { 382 | return err 383 | } 384 | 385 | for index := range explain { 386 | processExplain(&explain[index]) 387 | writeExplain(writer, &explain[index], width) 388 | } 389 | 390 | return nil 391 | } 392 | -------------------------------------------------------------------------------- /pybindings/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/simon-engledew/gocmdpev/pybindings 2 | 3 | go 1.14 4 | 5 | replace github.com/simon-engledew/gocmdpev/pev => ../pev 6 | 7 | require github.com/simon-engledew/gocmdpev/pev v0.0.0-20190513135149-ceda29aa18fc 8 | -------------------------------------------------------------------------------- /pybindings/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 2 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 3 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 4 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 5 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 6 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 7 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 8 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 9 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 10 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 11 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 12 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 13 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= 14 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 15 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 16 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 17 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 18 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 19 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 20 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 21 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 22 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.0.0-20200316230553-a7d97aace0b0 h1:4Khi5GeNOkZS5DqSBRn4Sy7BE6GuxwOqARPqfurkdNk= 26 | golang.org/x/sys v0.0.0-20200316230553-a7d97aace0b0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | -------------------------------------------------------------------------------- /pybindings/pycmdpev.go: -------------------------------------------------------------------------------- 1 | package main // import "github.com/simon-engledew/gocmdpev/pybindings" 2 | 3 | /* 4 | #define Py_LIMITED_API 5 | #include 6 | 7 | PyObject * visualize(PyObject *, PyObject *); 8 | 9 | // Workaround missing variadic function support 10 | // https://github.com/golang/go/issues/975 11 | int PyArg_ParseTuple_s(PyObject * args, const char **a) { 12 | return PyArg_ParseTuple(args, "s", a); 13 | } 14 | 15 | static PyMethodDef methods[] = { 16 | { "visualize", visualize, METH_VARARGS, "Visualise a JSON explain" }, 17 | { NULL, NULL, 0, NULL } 18 | }; 19 | 20 | static struct PyModuleDef module = { 21 | PyModuleDef_HEAD_INIT, "pycmdpev", NULL, -1, methods 22 | }; 23 | 24 | PyObject * ReturnNone(void) { 25 | Py_RETURN_NONE; 26 | } 27 | 28 | PyMODINIT_FUNC 29 | PyInit_pycmdpev(void) 30 | { 31 | return PyModule_Create(&module); 32 | } 33 | */ 34 | import "C" 35 | 36 | import "errors" 37 | 38 | func ArgsString(args *C.PyObject) (string, error) { 39 | var a *C.char 40 | 41 | if C.PyArg_ParseTuple_s(args, &a) == 0 { 42 | return "", errors.New("ArgumentError") 43 | } 44 | 45 | return C.GoString(a), nil 46 | } 47 | 48 | func ReturnNone() (*C.PyObject) { 49 | return C.ReturnNone() 50 | } 51 | 52 | func main() {} 53 | -------------------------------------------------------------------------------- /pybindings/python3.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/simon-engledew/gocmdpev/pev" 9 | ) 10 | 11 | /* 12 | #cgo pkg-config: python3 13 | #define Py_LIMITED_API 14 | #include 15 | */ 16 | import "C" 17 | 18 | //export visualize 19 | func visualize(self, args *C.PyObject) *C.PyObject { 20 | input, err := ArgsString(args) 21 | 22 | if err != nil { 23 | return nil 24 | } 25 | 26 | err = pev.Visualize(os.Stdout, strings.NewReader(input), 60) 27 | 28 | if err != nil { 29 | C.PyErr_SetString(C.PyExc_RuntimeError, C.CString(fmt.Sprintf("%v", err))) 30 | } 31 | 32 | return ReturnNone() 33 | } 34 | --------------------------------------------------------------------------------