Skip to content

Commit 02ecd6a

Browse files
committed
Synced from main
2 parents 97f637a + 82c7d92 commit 02ecd6a

File tree

9 files changed

+255
-179
lines changed

9 files changed

+255
-179
lines changed

changelog.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
Upcoming (TBD)
1+
TBD
22
==============
33

44
Features
55
--------
6-
* Make password options also function as flags. Reworked password logic to prompt user as early as possible. (#341):
6+
* Make password options also function as flags. Reworked password logic to prompt user as early as possible (#341).
7+
* More complete and up-to-date set of MySQL reserved words for completions.
8+
* Place exact-leading completions first.
9+
* Allow history file location to be configured.
10+
11+
12+
Bug Fixes
13+
--------
14+
* Respect `--logfile` when using `--execute` or standard input at the shell CLI.
15+
* Gracefully catch Paramiko parsing errors on `--list-ssh-config`.
16+
* Downgrade to Paramiko 3.5.1 to avoid crashing on DSA SSH keys.
717

818

919
1.44.2 (2026/01/13)

mycli/main.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ def run_cli(self) -> None:
732732
if self.smart_completion:
733733
self.refresh_completions()
734734

735-
history_file = os.path.expanduser(os.environ.get("MYCLI_HISTFILE", "~/.mycli-history"))
735+
history_file = os.path.expanduser(os.environ.get("MYCLI_HISTFILE", self.config.get("history_file", "~/.mycli-history")))
736736
if dir_path_exists(history_file):
737737
history = FileHistoryWithTimestamp(history_file)
738738
else:
@@ -952,10 +952,7 @@ def one_iteration(text: str | None = None) -> None:
952952
logger.debug("sql: %r", text)
953953

954954
special.write_tee(self.get_prompt(self.prompt_format) + text)
955-
if self.logfile:
956-
self.logfile.write(f"\n# {datetime.now()}\n")
957-
self.logfile.write(text)
958-
self.logfile.write("\n")
955+
self.log_query(text)
959956

960957
successful = False
961958
start = time()
@@ -1136,6 +1133,12 @@ def reconnect(self, database: str = "") -> bool:
11361133
self.echo(str(e), err=True, fg="red")
11371134
return False
11381135

1136+
def log_query(self, query: str) -> None:
1137+
if isinstance(self.logfile, TextIOWrapper):
1138+
self.logfile.write(f"\n# {datetime.now()}\n")
1139+
self.logfile.write(query)
1140+
self.logfile.write("\n")
1141+
11391142
def log_output(self, output: str) -> None:
11401143
"""Log the output in the audit log, if it's enabled."""
11411144
if isinstance(self.logfile, TextIOWrapper):
@@ -1315,6 +1318,7 @@ def get_prompt(self, string: str) -> str:
13151318
def run_query(self, query: str, new_line: bool = True) -> None:
13161319
"""Runs *query*."""
13171320
assert self.sqlexecute is not None
1321+
self.log_query(query)
13181322
results = self.sqlexecute.run(query)
13191323
for result in results:
13201324
title = result.title
@@ -1331,6 +1335,7 @@ def run_query(self, query: str, new_line: bool = True) -> None:
13311335
self.null_string,
13321336
)
13331337
for line in output:
1338+
self.log_output(line)
13341339
click.echo(line, nl=new_line)
13351340

13361341
# get and display warnings if enabled
@@ -1623,12 +1628,17 @@ def cli(
16231628
sys.exit(0)
16241629
if list_ssh_config:
16251630
ssh_config = read_ssh_config(ssh_config_path)
1626-
for host in ssh_config.get_hostnames():
1631+
try:
1632+
host_entries = ssh_config.get_hostnames()
1633+
except KeyError:
1634+
click.secho('Error reading ssh config', err=True, fg="red")
1635+
sys.exit(1)
1636+
for host_entry in host_entries:
16271637
if verbose:
1628-
host_config = ssh_config.lookup(host)
1629-
click.secho(f"{host} : {host_config.get('hostname')}")
1638+
host_config = ssh_config.lookup(host_entry)
1639+
click.secho(f"{host_entry} : {host_config.get('hostname')}")
16301640
else:
1631-
click.secho(host)
1641+
click.secho(host_entry)
16321642
sys.exit(0)
16331643
# Choose which ever one has a valid value.
16341644
database = dbname or database

mycli/myclirc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ multi_line = False
2727
# or "shutdown".
2828
destructive_warning = True
2929

30+
# interactive query history location.
31+
history_file = ~/.mycli-history
32+
3033
# log_file location.
3134
log_file = ~/.mycli.log
3235

mycli/sqlcompleter.py

Lines changed: 28 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
99
from prompt_toolkit.completion.base import Document
10+
from pygments.lexers._mysql_builtins import MYSQL_DATATYPES, MYSQL_FUNCTIONS, MYSQL_KEYWORDS
1011

1112
from mycli.packages.completion_engine import suggest_type
1213
from mycli.packages.filepaths import complete_path, parse_path, suggest_path
@@ -18,141 +19,27 @@
1819

1920

2021
class SQLCompleter(Completer):
22+
favorite_keywords = [
23+
'SELECT',
24+
'FROM',
25+
'WHERE',
26+
'UPDATE',
27+
'DELETE FROM',
28+
'GROUP BY',
29+
'ORDER BY',
30+
'JOIN',
31+
'INSERT INTO',
32+
'LIKE',
33+
'LIMIT',
34+
]
2135
keywords = [
22-
"SELECT",
23-
"FROM",
24-
"WHERE",
25-
"UPDATE",
26-
"DELETE FROM",
27-
"GROUP BY",
28-
"JOIN",
29-
"INSERT INTO",
30-
"LIKE",
31-
"LIMIT",
32-
"ACCESS",
33-
"ADD",
34-
"ALL",
35-
"ALTER TABLE",
36-
"AND",
37-
"ANY",
38-
"AS",
39-
"ASC",
40-
"AUTO_INCREMENT",
41-
"BEFORE",
42-
"BEGIN",
43-
"BETWEEN",
44-
"BIGINT",
45-
"BINARY",
46-
"BY",
47-
"CASE",
48-
"CHANGE MASTER TO",
49-
"CHAR",
50-
"CHARACTER SET",
51-
"CHECK",
52-
"COLLATE",
53-
"COLUMN",
54-
"COMMENT",
55-
"COMMIT",
56-
"CONSTRAINT",
57-
"CREATE",
58-
"CURRENT",
59-
"CURRENT_TIMESTAMP",
60-
"DATABASE",
61-
"DATE",
62-
"DECIMAL",
63-
"DEFAULT",
64-
"DESC",
65-
"DESCRIBE",
66-
"DROP",
67-
"ELSE",
68-
"END",
69-
"ENGINE",
70-
"ESCAPE",
71-
"EXISTS",
72-
"FILE",
73-
"FLOAT",
74-
"FOR",
75-
"FOREIGN KEY",
76-
"FORMAT",
77-
"FULL",
78-
"FUNCTION",
79-
"GRANT",
80-
"HAVING",
81-
"HOST",
82-
"IDENTIFIED",
83-
"IN",
84-
"INCREMENT",
85-
"INDEX",
86-
"INT",
87-
"INTEGER",
88-
"INTERVAL",
89-
"INTO",
90-
"IS",
91-
"KEY",
92-
"LEFT",
93-
"LEVEL",
94-
"LOCK",
95-
"LOGS",
96-
"LONG",
97-
"MASTER",
98-
"MEDIUMINT",
99-
"MODE",
100-
"MODIFY",
101-
"NOT",
102-
"NULL",
103-
"NUMBER",
104-
"OFFSET",
105-
"ON",
106-
"OPTION",
107-
"OR",
108-
"ORDER BY",
109-
"OUTER",
110-
"OWNER",
111-
"PASSWORD",
112-
"PORT",
113-
"PRIMARY",
114-
"PRIVILEGES",
115-
"PROCESSLIST",
116-
"PURGE",
117-
"REFERENCES",
118-
"REGEXP",
119-
"RENAME",
120-
"REPAIR",
121-
"RESET",
122-
"REVOKE",
123-
"RIGHT",
124-
"ROLLBACK",
125-
"ROW",
126-
"ROWS",
127-
"ROW_FORMAT",
128-
"SAVEPOINT",
129-
"SESSION",
130-
"SET",
131-
"SHARE",
132-
"SHOW",
133-
"SLAVE",
134-
"SMALLINT",
135-
"START",
136-
"STOP",
137-
"TABLE",
138-
"THEN",
139-
"TINYINT",
140-
"TO",
141-
"TRANSACTION",
142-
"TRIGGER",
143-
"TRUNCATE",
144-
"UNION",
145-
"UNIQUE",
146-
"UNSIGNED",
147-
"USE",
148-
"USER",
149-
"USING",
150-
"VALUES",
151-
"VARCHAR",
152-
"VIEW",
153-
"WHEN",
154-
"WITH",
36+
x.upper()
37+
for x in favorite_keywords
38+
+ list(MYSQL_DATATYPES)
39+
+ list(MYSQL_KEYWORDS)
40+
+ ['ALTER TABLE', 'CHANGE MASTER TO', 'CHARACTER SET', 'FOREIGN KEY']
15541
]
42+
keywords = list(dict.fromkeys(keywords))
15643

15744
tidb_keywords = [
15845
"SELECT",
@@ -838,27 +725,7 @@ class SQLCompleter(Completer):
838725
"ZEROFILL",
839726
]
840727

841-
functions = [
842-
"AVG",
843-
"CONCAT",
844-
"COUNT",
845-
"DISTINCT",
846-
"FIRST",
847-
"FORMAT",
848-
"FROM_UNIXTIME",
849-
"LAST",
850-
"LCASE",
851-
"LEN",
852-
"MAX",
853-
"MID",
854-
"MIN",
855-
"NOW",
856-
"ROUND",
857-
"SUM",
858-
"TOP",
859-
"UCASE",
860-
"UNIX_TIMESTAMP",
861-
]
728+
functions = [x.upper() for x in MYSQL_FUNCTIONS]
862729

863730
# https://docs.pingcap.com/tidb/dev/tidb-functions
864731
tidb_functions = [
@@ -1110,6 +977,13 @@ def apply_case(kw: str) -> str:
1110977
return kw.upper()
1111978
return kw.lower()
1112979

980+
def exact_leading_key(item: tuple[int, int, str], text):
981+
if text and item[2].lower().startswith(text):
982+
return -1000 + len(item[2])
983+
return 0
984+
985+
completions = sorted(completions, key=lambda item: exact_leading_key(item, text))
986+
1113987
return (Completion(z if casing is None else apply_case(z), -len(text)) for x, y, z in completions)
1114988

1115989
def get_completions(

pyproject.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ urls = { homepage = "http://mycli.net" }
1111
dependencies = [
1212
"click >= 8.3.1",
1313
"cryptography >= 1.0.0",
14-
"Pygments>=1.6",
14+
"Pygments ~= 2.19.2",
1515
"prompt_toolkit>=3.0.6,<4.0.0",
1616
"PyMySQL >= 0.9.2",
1717
"sqlparse>=0.3.0,<0.6.0",
@@ -31,7 +31,10 @@ build-backend = "setuptools.build_meta"
3131

3232

3333
[project.optional-dependencies]
34-
ssh = ["paramiko", "sshtunnel"]
34+
ssh = [
35+
"paramiko~=3.5.1",
36+
"sshtunnel",
37+
]
3538
llm = [
3639
"llm>=0.19.0",
3740
"setuptools", # Required by llm commands to install models
@@ -50,7 +53,7 @@ dev = [
5053
"pytest-cov>=4.1.0",
5154
"tox>=4.8.0",
5255
"pdbpp>=0.10.3",
53-
"paramiko",
56+
"paramiko~=3.5.1",
5457
"sshtunnel",
5558
"llm>=0.19.0",
5659
"setuptools", # Required by llm commands to install models

test/myclirc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ multi_line = False
2727
# or "shutdown".
2828
destructive_warning = True
2929

30+
# interactive query history location.
31+
history_file = ~/.mycli-history
32+
3033
# log_file location.
3134
log_file = ~/.mycli.test.log
3235

test/test_main.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,3 +878,22 @@ def test_global_init_commands(executor):
878878
expected = "sql_select_limit\t9999\n"
879879
assert result.exit_code == 0
880880
assert expected in result.output
881+
882+
883+
@dbtest
884+
def test_execute_with_logfile(executor):
885+
"""Test that --execute combines with --logfile"""
886+
sql = 'select 1'
887+
runner = CliRunner()
888+
889+
with NamedTemporaryFile(mode="w", delete=False) as logfile:
890+
result = runner.invoke(mycli.main.cli, args=CLI_ARGS + ["--logfile", logfile.name, "--execute", sql])
891+
assert result.exit_code == 0
892+
893+
assert os.path.getsize(logfile.name) > 0
894+
895+
try:
896+
if os.path.exists(logfile.name):
897+
os.remove(logfile.name)
898+
except Exception as e:
899+
print(f"An error occurred while attempting to delete the file: {e}")

0 commit comments

Comments
 (0)