Skip to content

Commit 1b779dc

Browse files
committed
Create CLI wrapper
* Add README.md with Debian instructions Code fixes * Added `.encode()` to handle `str` to `byte` conversion * Removed `is_supported_mysql_version_at_least()` check * Fixed out-parameter handling for `cairo.cairo_text_extents()` * Add missing argument to `cairo.cairo_set_dash()`
1 parent 36cd1db commit 1b779dc

File tree

9 files changed

+206
-17
lines changed

9 files changed

+206
-17
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# MySQL Visual Explain CLI
2+
3+
[MySQL Workbench](https://github.com/mysql/mysql-workbench/) comes with a Visual Explain feature that is written in Python using [Cairo](https://www.cairographics.org/). This project is simply extracts that code and provides a minimal CLI for it.
4+
5+
## Usages
6+
7+
### Debian
8+
9+
```sh
10+
sudo apt-get install python3-cairocffi
11+
12+
python3 ./mysql_visual_explain_cli explain.json explain.png
13+
```
14+
15+
### Poetry
16+
17+
```sh
18+
poetry run python ./mysql_visual_explain_cli explain.json explain.png
19+
```
20+
21+
## Notes
22+
23+
The version of Python bundled with MySQL Workbench also seems to perform implicit `str` to `byte` conversion.
24+
25+
The MySQL Workbench code does not use the common Pycairo library, but instead calls Cairo C directly via FFI. The following C functions appear to have been patched or used a different version compare to what is distrubted with modern Debian distributions.
26+
27+
* `cairo.cairo_text_extents()`
28+
* `cairo.cairo_set_dash()`
29+
30+
## License
31+
32+
This project is a derivative work of MySQL Workbench which is GPLv2. Meaning unless I have money for a lawyer, it will forever be GPLv2 as well.

mysql_visual_explain_cli/__init__.py

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import argparse
2+
3+
from query_analysis import explain_renderer
4+
5+
6+
def render(json_data, output_path):
7+
ctx = explain_renderer.ExplainContext(explain_renderer.decode_json(json_data), None)
8+
ctx.init_canvas(None, None, lambda x, y, w, h: None)
9+
ctx.layout()
10+
ctx.export_to_png(output_path)
11+
12+
def main():
13+
parser = argparse.ArgumentParser(description="CLI to convert MySQL JSON EXPLAIN to PNG file.")
14+
parser.add_argument('infile', type=argparse.FileType('r'), help="Input file (must be JSON)")
15+
parser.add_argument('outfile', help="Output file path (output is PNG)")
16+
args = parser.parse_args()
17+
18+
with args.infile as f:
19+
json_data = f.read().replace('\n','')
20+
render(json_data, args.outfile)
21+
22+
if __name__ == '__main__':
23+
main()

mysql_visual_explain_cli/graphics/__init__.py

Whitespace-only changes.

mysql_visual_explain_cli/graphics/cairo_utils.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
# along with this program; if not, write to the Free Software Foundation, Inc.,
2020
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
2121

22-
import cairo
22+
import cairocffi
23+
from cairocffi import cairo
24+
2325
from math import ceil, floor, fabs, atan, pi
2426

2527

@@ -31,7 +33,7 @@ def status(self):
3133
return cairo.cairo_surface_status(self.s)
3234

3335
def write_to_png(self, file):
34-
cairo.cairo_surface_write_to_png(self.s, file)
36+
cairo.cairo_surface_write_to_png(self.s, file.encode())
3537

3638

3739
class ImageSurface(Surface):
@@ -125,10 +127,10 @@ def restore(self):
125127
cairo.cairo_restore(self.cr)
126128

127129
def set_dash(self, dashes, offset):
128-
cairo.cairo_set_dash(self.cr, dashes, offset)
130+
cairo.cairo_set_dash(self.cr, dashes, 1, offset)
129131

130132
def set_font(self, family, italic=False, bold=False):
131-
cairo.cairo_select_font_face(self.cr, family, cairo.CAIRO_FONT_SLANT_ITALIC if italic else cairo.CAIRO_FONT_SLANT_NORMAL, cairo.CAIRO_FONT_WEIGHT_BOLD if bold else cairo.CAIRO_FONT_WEIGHT_NORMAL)
133+
cairo.cairo_select_font_face(self.cr, family.encode(), cairo.CAIRO_FONT_SLANT_ITALIC if italic else cairo.CAIRO_FONT_SLANT_NORMAL, cairo.CAIRO_FONT_WEIGHT_BOLD if bold else cairo.CAIRO_FONT_WEIGHT_NORMAL)
132134

133135
def set_font_size(self, size):
134136
cairo.cairo_set_font_size(self.cr, size)
@@ -155,7 +157,7 @@ def mask_surface(self, surface, x, y):
155157
cairo.cairo_mask_surface(self.cr, surface.s, x, y)
156158

157159
def show_text(self, text):
158-
cairo.cairo_show_text(self.cr, text)
160+
cairo.cairo_show_text(self.cr, text.encode())
159161

160162
def rounded_rect(self, x, y, w, h, r):
161163
self.move_to(x+r, y)
@@ -202,7 +204,9 @@ def stroke_preserve(self):
202204
cairo.cairo_stroke_preserve(self.cr)
203205

204206
def text_extents(self, text):
205-
return cairo.cairo_text_extents(self.cr, text)
207+
extents = cairocffi.ffi.new('cairo_text_extents_t *')
208+
cairo.cairo_text_extents(self.cr, text.encode(), extents)
209+
return extents
206210

207211

208212

mysql_visual_explain_cli/query_analysis/__init__.py

Whitespace-only changes.

mysql_visual_explain_cli/query_analysis/explain_renderer.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,18 @@
1919
# along with this program; if not, write to the Free Software Foundation, Inc.,
2020
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
2121

22-
import mforms
23-
22+
import io
23+
import json
24+
import logging
2425

25-
from workbench.log import log_error
26+
from cairocffi import cairo
2627

28+
from graphics.canvas import VBoxFigure, Canvas, DiamondShapeFigure, RectangleShapeFigure, TextFigure, HFill, draw_varrow, draw_harrow
29+
from graphics.cairo_utils import ImageSurface, Context
2730

28-
from workbench.graphics.canvas import VBoxFigure, Canvas, DiamondShapeFigure, RectangleShapeFigure, TextFigure, HFill, draw_varrow, draw_harrow
29-
from workbench.graphics.cairo_utils import ImageSurface, Context
30-
import cairo
3131

32-
import io
33-
import json
32+
def log_error(error):
33+
logging.exception(error)
3434

3535

3636
def decode_json(text):
@@ -425,9 +425,7 @@ def vconnect_pos_offset(self):
425425

426426
@property
427427
def rows_count(self):
428-
if self._context.server_version.is_supported_mysql_version_at_least(5, 7):
429-
return self.child_below.rows_produced
430-
return None
428+
return self.child_below.rows_produced
431429

432430

433431
def get_read_eval_cost(self):

poetry.lock

Lines changed: 115 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[tool.poetry]
2+
name = "mysql-visual-explain-cli"
3+
version = "0.1.0"
4+
description = ""
5+
authors = ["Leon Yu <leon@leonyu.net>"]
6+
license = "GPL-2.0-only"
7+
readme = "README.md"
8+
packages = [{include = "mysql_visual_explain_cli"}]
9+
10+
[tool.poetry.dependencies]
11+
python = "^3.11"
12+
cairocffi = "^1.6.1"
13+
14+
15+
[build-system]
16+
requires = ["poetry-core"]
17+
build-backend = "poetry.core.masonry.api"

0 commit comments

Comments
 (0)