Skip to content

Commit f37f737

Browse files
authored
Merge pull request #1868 from dbcli/RW/extend-and-align-status-output
Extend `status` command output
2 parents bcd4092 + 0d23663 commit f37f737

5 files changed

Lines changed: 201 additions & 31 deletions

File tree

changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Upcoming (TBD)
22
==============
33

4+
Features
5+
---------
6+
* Add more output to the `status` command.
7+
8+
49
Documentation
510
---------
611
* Give example for ANSI prompt colors in `~/.myclirc`.

mycli/packages/special/dbcommands.py

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
from mycli import __version__
99
from mycli.packages.special import iocommands
1010
from mycli.packages.special.main import ArgType, special_command
11-
from mycli.packages.special.utils import format_uptime, get_ssl_version
11+
from mycli.packages.special.utils import (
12+
format_uptime,
13+
get_local_timezone,
14+
get_server_timezone,
15+
get_ssl_cipher,
16+
get_ssl_version,
17+
)
1218
from mycli.packages.sqlresult import SQLResult
1319

1420
logger = logging.getLogger(__name__)
@@ -69,7 +75,7 @@ def status(cur: Cursor, **_) -> list[SQLResult]:
6975
try:
7076
cur.execute(query)
7177
except ProgrammingError:
72-
# Fallback in case query fail, as it does with Mysql 4
78+
# Fallback in case query fails, as it does with Mysql 4
7379
query = "SHOW STATUS;"
7480
logger.debug(query)
7581
cur.execute(query)
@@ -78,15 +84,24 @@ def status(cur: Cursor, **_) -> list[SQLResult]:
7884
query = "SHOW GLOBAL VARIABLES;"
7985
logger.debug(query)
8086
cur.execute(query)
81-
variables = dict(cur.fetchall())
87+
global_variables = dict(cur.fetchall())
8288

83-
# prepare in case keys are bytes, as with Python 3 and Mysql 4
84-
if isinstance(list(variables)[0], bytes) and isinstance(list(status)[0], bytes):
85-
variables = {k.decode("utf-8"): v.decode("utf-8") for k, v in variables.items()}
89+
query = "SHOW SESSION VARIABLES;"
90+
logger.debug(query)
91+
cur.execute(query)
92+
session_variables = dict(cur.fetchall())
93+
94+
# decode in case keys are bytes, as with Mysql 4
95+
if global_variables and isinstance(list(global_variables)[0], bytes):
96+
global_variables = {k.decode("utf-8"): v.decode("utf-8") for k, v in global_variables.items()}
97+
if session_variables and isinstance(list(session_variables)[0], bytes):
98+
session_variables = {k.decode("utf-8"): v.decode("utf-8") for k, v in session_variables.items()}
99+
if status and isinstance(list(status)[0], bytes):
86100
status = {k.decode("utf-8"): v.decode("utf-8") for k, v in status.items()}
87101

88102
# Create output buffers.
89103
preamble = []
104+
header = ['Setting', 'Value']
90105
output = []
91106
footer = []
92107

@@ -111,7 +126,6 @@ def status(cur: Cursor, **_) -> list[SQLResult]:
111126
else:
112127
db = ""
113128
user = ""
114-
115129
output.append(("Current database:", db))
116130
output.append(("Current user:", user))
117131

@@ -124,9 +138,16 @@ def status(cur: Cursor, **_) -> list[SQLResult]:
124138
pager = "stdout"
125139
output.append(("Current pager:", pager))
126140

127-
output.append(("Server version:", f'{variables["version"]} {variables["version_comment"]}'))
128-
output.append(("Protocol version:", variables["protocol_version"]))
129-
output.append(('SSL/TLS version:', get_ssl_version(cur)))
141+
output.append(("Using delimiter:", iocommands.get_current_delimiter()))
142+
output.append(("Using outfile:", iocommands.tee_file.name if iocommands.tee_file else ''))
143+
144+
output.append(("Server version:", f'{global_variables["version"]} {global_variables["version_comment"]}'))
145+
output.append(("Protocol version:", global_variables["protocol_version"]))
146+
if cipher := get_ssl_cipher(cur):
147+
output.append(('SSL:', f'Cipher in use is {cipher}'))
148+
else:
149+
output.append(('SSL:', ''))
150+
output.append(('SSL/TLS version:', get_ssl_version(cur) or ''))
130151

131152
if getattr(cur.connection, 'unix_socket', None):
132153
host_info = cur.connection.host_info
@@ -135,23 +156,28 @@ def status(cur: Cursor, **_) -> list[SQLResult]:
135156

136157
output.append(("Connection:", host_info))
137158

138-
query = "SELECT @@character_set_server, @@character_set_database, @@character_set_client, @@character_set_connection LIMIT 1;"
139-
logger.debug(query)
140-
cur.execute(query)
141-
if one := cur.fetchone():
142-
charset = one
143-
else:
144-
charset = ("", "", "", "")
145-
output.append(("Server characterset:", charset[0]))
146-
output.append(("Db characterset:", charset[1]))
147-
output.append(("Client characterset:", charset[2]))
148-
output.append(("Conn. characterset:", charset[3]))
159+
charset_spec = [
160+
{'name': 'Server characterset:', 'variable': 'character_set_server'},
161+
{'name': 'Db characterset:', 'variable': 'character_set_database'},
162+
{'name': 'Client characterset:', 'variable': 'character_set_client'},
163+
{'name': 'Conn. characterset:', 'variable': 'character_set_connection'},
164+
{'name': 'Result characterset:', 'variable': 'character_set_results'},
165+
]
166+
for elt in charset_spec:
167+
if elt['variable'] in session_variables:
168+
value = session_variables[elt['variable']]
169+
else:
170+
value = ''
171+
output.append((elt['name'], value))
149172

150173
if getattr(cur.connection, 'unix_socket', None):
151-
output.append(('UNIX socket:', variables['socket']))
174+
output.append(('UNIX socket:', global_variables['socket']))
152175
else:
153176
output.append(('TCP port:', cur.connection.port))
154177

178+
output.append(('Server timezone:', get_server_timezone(global_variables)))
179+
output.append(('Local timezone:', get_local_timezone()))
180+
155181
if "Uptime" in status:
156182
output.append(("Uptime:", format_uptime(status["Uptime"])))
157183

@@ -174,4 +200,4 @@ def status(cur: Cursor, **_) -> list[SQLResult]:
174200

175201
footer.append("--------------")
176202

177-
return [SQLResult(preamble="\n".join(preamble), rows=output, postamble="\n".join(footer))]
203+
return [SQLResult(preamble="\n".join(preamble), header=header, rows=output, postamble="\n".join(footer))]

mycli/packages/special/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import datetime
12
import logging
23
import os
4+
from typing import Any
35

46
import click
57
import pymysql
@@ -110,3 +112,34 @@ def get_ssl_version(cur: Cursor) -> str | None:
110112
pass
111113

112114
return ssl_version
115+
116+
117+
def get_ssl_cipher(cur: Cursor) -> str | None:
118+
query = 'SHOW STATUS LIKE "Ssl_cipher"'
119+
logger.debug(query)
120+
121+
ssl_cipher = None
122+
123+
try:
124+
cur.execute(query)
125+
if one := cur.fetchone():
126+
ssl_cipher = one[1] or None
127+
except pymysql.err.OperationalError:
128+
pass
129+
130+
return ssl_cipher
131+
132+
133+
def get_server_timezone(variables: dict[str, Any]) -> str:
134+
try:
135+
if variables['time_zone'] == 'SYSTEM':
136+
server_tz = variables['system_time_zone']
137+
else:
138+
server_tz = variables['time_zone']
139+
return server_tz
140+
except KeyError:
141+
return ''
142+
143+
144+
def get_local_timezone() -> str:
145+
return datetime.datetime.now().astimezone().tzname() or ''

test/pytests/test_special_dbcommands.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ def test_status_uses_global_queries_decodes_bytes_and_formats_stats(monkeypatch)
182182
monkeypatch.setattr(dbcommands.platform, 'python_implementation', lambda: 'CPython')
183183
monkeypatch.setattr(dbcommands.platform, 'python_version', lambda: '3.14.0')
184184
monkeypatch.setattr(dbcommands.iocommands, 'is_pager_enabled', lambda: True)
185+
monkeypatch.setattr(dbcommands, 'get_ssl_cipher', lambda cur: 'TLS_AES_256_GCM_SHA384')
185186
monkeypatch.setattr(dbcommands, 'get_ssl_version', lambda cur: 'TLSv1.3')
186187
monkeypatch.setattr(dbcommands, 'format_uptime', lambda uptime: f'{uptime} seconds')
187188
monkeypatch.setenv('PAGER', 'less -SR')
@@ -210,8 +211,14 @@ def test_status_uses_global_queries_decodes_bytes_and_formats_stats(monkeypatch)
210211
'SELECT DATABASE(), USER();': {
211212
'rows': [('test_db', 'test_user')],
212213
},
213-
'SELECT @@character_set_server, @@character_set_database, @@character_set_client, @@character_set_connection LIMIT 1;': {
214-
'rows': [('utf8mb4', 'utf8mb4', 'utf8mb4', 'utf8mb4')],
214+
'SHOW SESSION VARIABLES;': {
215+
'rows': [
216+
(b'character_set_server', b'utf8mb4'),
217+
(b'character_set_database', b'utf8mb4'),
218+
(b'character_set_client', b'utf8mb4'),
219+
(b'character_set_connection', b'utf8mb4'),
220+
(b'character_set_results', b'utf8mb4'),
221+
],
215222
},
216223
},
217224
)
@@ -225,6 +232,7 @@ def test_status_uses_global_queries_decodes_bytes_and_formats_stats(monkeypatch)
225232
assert ('Current pager:', 'less -SR') in result.rows
226233
assert ('Server version:', '8.0.0 Community') in result.rows
227234
assert ('Protocol version:', '10') in result.rows
235+
assert ('SSL:', 'Cipher in use is TLS_AES_256_GCM_SHA384') in result.rows
228236
assert ('SSL/TLS version:', 'TLSv1.3') in result.rows
229237
assert ('Connection:', 'tcp-host via TCP/IP') in result.rows
230238
assert ('TCP port:', 3307) in result.rows
@@ -264,10 +272,10 @@ def test_status_falls_back_to_show_status_and_handles_empty_selects(monkeypatch)
264272
('socket', '/tmp/mysql.sock'),
265273
],
266274
},
267-
'SELECT DATABASE(), USER();': {
275+
'SHOW SESSION VARIABLES;': {
268276
'rows': [],
269277
},
270-
'SELECT @@character_set_server, @@character_set_database, @@character_set_client, @@character_set_connection LIMIT 1;': {
278+
'SELECT DATABASE(), USER();': {
271279
'rows': [],
272280
},
273281
},
@@ -282,9 +290,9 @@ def test_status_falls_back_to_show_status_and_handles_empty_selects(monkeypatch)
282290
assert ('Connection:', 'Localhost via UNIX socket') in result.rows
283291
assert ('UNIX socket:', '/tmp/mysql.sock') in result.rows
284292
assert ('Server characterset:', '') in result.rows
285-
assert ('Db characterset:', '') in result.rows
293+
assert ('Db characterset:', '') in result.rows
286294
assert ('Client characterset:', '') in result.rows
287-
assert ('Conn. characterset:', '') in result.rows
295+
assert ('Conn. characterset:', '') in result.rows
288296
assert 'Connections:' not in result.postamble
289297
assert '--------------' in result.postamble
290298

@@ -307,8 +315,14 @@ def test_status_uses_system_default_pager_when_enabled_without_env(monkeypatch)
307315
'SELECT DATABASE(), USER();': {
308316
'rows': [('db', 'user')],
309317
},
310-
'SELECT @@character_set_server, @@character_set_database, @@character_set_client, @@character_set_connection LIMIT 1;': {
311-
'rows': [('utf8', 'utf8', 'utf8', 'utf8')],
318+
'SHOW SESSION VARIABLES;': {
319+
'rows': [
320+
('character_set_server', 'utf8'),
321+
('character_set_database', 'utf8'),
322+
('character_set_client', 'utf8'),
323+
('character_set_connection', 'utf8'),
324+
('character_set_results', 'utf8'),
325+
],
312326
},
313327
},
314328
)

test/pytests/test_special_utils.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from mycli.packages.special.utils import (
1313
CACHED_SSL_VERSION,
1414
format_uptime,
15+
get_local_timezone,
16+
get_server_timezone,
17+
get_ssl_cipher,
1518
get_ssl_version,
1619
get_uptime,
1720
get_warning_count,
@@ -185,3 +188,92 @@ def test_get_ssl_version_ignores_operational_error() -> None:
185188
cur.execute.side_effect = pymysql.err.OperationalError()
186189

187190
assert get_ssl_version(cur) is None
191+
192+
193+
def test_get_ssl_cipher_returns_value() -> None:
194+
cur = MagicMock()
195+
cur.fetchone.return_value = ('Ssl_cipher', 'TLS_AES_256_GCM_SHA384')
196+
197+
ssl_cipher = get_ssl_cipher(cur)
198+
199+
cur.execute.assert_called_once_with('SHOW STATUS LIKE "Ssl_cipher"')
200+
assert ssl_cipher == 'TLS_AES_256_GCM_SHA384'
201+
202+
203+
def test_get_ssl_cipher_returns_none_for_missing_row() -> None:
204+
cur = MagicMock()
205+
cur.fetchone.return_value = None
206+
207+
assert get_ssl_cipher(cur) is None
208+
209+
210+
def test_get_ssl_cipher_returns_none_for_empty_value() -> None:
211+
cur = MagicMock()
212+
cur.fetchone.return_value = ('Ssl_cipher', '')
213+
214+
assert get_ssl_cipher(cur) is None
215+
216+
217+
def test_get_ssl_cipher_ignores_operational_error() -> None:
218+
cur = MagicMock()
219+
cur.execute.side_effect = pymysql.err.OperationalError()
220+
221+
assert get_ssl_cipher(cur) is None
222+
223+
224+
def test_get_server_timezone_prefers_system_timezone_when_requested() -> None:
225+
variables = {
226+
'time_zone': 'SYSTEM',
227+
'system_time_zone': 'UTC',
228+
}
229+
230+
assert get_server_timezone(variables) == 'UTC'
231+
232+
233+
def test_get_server_timezone_returns_explicit_timezone() -> None:
234+
variables = {
235+
'time_zone': '+02:00',
236+
'system_time_zone': 'UTC',
237+
}
238+
239+
assert get_server_timezone(variables) == '+02:00'
240+
241+
242+
def test_get_server_timezone_returns_empty_string_when_keys_are_missing() -> None:
243+
assert get_server_timezone({}) == ''
244+
245+
246+
def test_get_local_timezone_returns_tzname(monkeypatch) -> None:
247+
class FakeAwareDatetime:
248+
def tzname(self) -> str:
249+
return 'EDT'
250+
251+
class FakeDatetime:
252+
@staticmethod
253+
def now() -> 'FakeDatetime':
254+
return FakeDatetime()
255+
256+
def astimezone(self) -> FakeAwareDatetime:
257+
return FakeAwareDatetime()
258+
259+
monkeypatch.setattr(mycli.packages.special.utils.datetime, 'datetime', FakeDatetime)
260+
261+
assert get_local_timezone() == 'EDT'
262+
263+
264+
def test_get_local_timezone_returns_empty_string_when_tzname_is_none(monkeypatch) -> None:
265+
class FakeAwareDatetime:
266+
def tzname(self) -> None:
267+
return None
268+
269+
class FakeDatetime:
270+
@staticmethod
271+
def now() -> 'FakeDatetime':
272+
return FakeDatetime()
273+
274+
def astimezone(self) -> FakeAwareDatetime:
275+
return FakeAwareDatetime()
276+
277+
monkeypatch.setattr(mycli.packages.special.utils.datetime, 'datetime', FakeDatetime)
278+
279+
assert get_local_timezone() == ''

0 commit comments

Comments
 (0)