1
0
Fork 0

Merging upstream version 1.26.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-09 19:02:15 +01:00
parent 79468558b6
commit c35ab76feb
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
23 changed files with 328 additions and 52 deletions

View file

@ -9,16 +9,16 @@ jobs:
linux: linux:
strategy: strategy:
matrix: matrix:
python-version: [3.6, 3.7, 3.8, 3.9] python-version: ['3.7', '3.8', '3.9', '3.10']
include: include:
- python-version: 3.6 - python-version: '3.7'
os: ubuntu-18.04 # MySQL 5.7.32 os: ubuntu-18.04 # MySQL 5.7.32
- python-version: 3.7 - python-version: '3.8'
os: ubuntu-18.04 # MySQL 5.7.32 os: ubuntu-18.04 # MySQL 5.7.32
- python-version: 3.8 - python-version: '3.9'
os: ubuntu-18.04 # MySQL 5.7.32
- python-version: 3.9
os: ubuntu-20.04 # MySQL 8.0.22 os: ubuntu-20.04 # MySQL 8.0.22
- python-version: '3.10'
os: ubuntu-22.04 # MySQL 8.0.28
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:

View file

@ -95,7 +95,7 @@ credentials to use by setting the applicable environment variables:
```bash ```bash
$ export PYTEST_HOST=localhost $ export PYTEST_HOST=localhost
$ export PYTEST_USER=user $ export PYTEST_USER=mycli
$ export PYTEST_PASSWORD=myclirocks $ export PYTEST_PASSWORD=myclirocks
$ export PYTEST_PORT=3306 $ export PYTEST_PORT=3306
$ export PYTEST_CHARSET=utf8 $ export PYTEST_CHARSET=utf8
@ -104,6 +104,14 @@ $ export PYTEST_CHARSET=utf8
The default values are `localhost`, `root`, no password, `3306`, and `utf8`. The default values are `localhost`, `root`, no password, `3306`, and `utf8`.
You only need to set the values that differ from the defaults. You only need to set the values that differ from the defaults.
If you would like to run the tests as a user with only the necessary privileges,
create a `mycli` user and run the following grant statements.
```sql
GRANT ALL PRIVILEGES ON `mycli_%`.* TO 'mycli'@'localhost';
GRANT SELECT ON mysql.* TO 'mycli'@'localhost';
GRANT SELECT ON performance_schema.* TO 'mycli'@'localhost';
```
### CLI Tests ### CLI Tests

View file

@ -69,6 +69,8 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
--ssh-config-host TEXT Host to connect to ssh server reading from ssh --ssh-config-host TEXT Host to connect to ssh server reading from ssh
configuration. configuration.
--ssl Enable SSL for connection (automatically
enabled with other flags).
--ssl-ca PATH CA file in PEM format. --ssl-ca PATH CA file in PEM format.
--ssl-capath TEXT CA directory. --ssl-capath TEXT CA directory.
--ssl-cert PATH X509 cert in PEM format. --ssl-cert PATH X509 cert in PEM format.
@ -133,6 +135,7 @@ Features
* Log every query and its results to a file (disabled by default). * Log every query and its results to a file (disabled by default).
* Pretty prints tabular data (with colors!) * Pretty prints tabular data (with colors!)
* Support for SSL connections * Support for SSL connections
* Some features are only exposed as [key bindings](doc/key_bindings.rst)
Contributions: Contributions:
-------------- --------------
@ -151,6 +154,22 @@ Twitter: [@amjithr](http://twitter.com/amjithr)
## Detailed Install Instructions: ## Detailed Install Instructions:
### Arch, Manjaro
You can install the mycli package available in the AUR:
```
$ yay -S mycli
```
### Debian, Ubuntu
On Debian, Ubuntu distributions, you can easily install the mycli package using apt:
```
$ sudo apt-get install mycli
```
### Fedora ### Fedora
Fedora has a package available for mycli, install it using dnf: Fedora has a package available for mycli, install it using dnf:
@ -164,13 +183,13 @@ $ sudo dnf install mycli
I haven't built an RPM package for mycli for RHEL or Centos yet. So please use `pip` to install `mycli`. You can install pip on your system using: I haven't built an RPM package for mycli for RHEL or Centos yet. So please use `pip` to install `mycli`. You can install pip on your system using:
``` ```
$ sudo yum install python-pip $ sudo yum install python3-pip
``` ```
Once that is installed, you can install mycli as follows: Once that is installed, you can install mycli as follows:
``` ```
$ sudo pip install mycli $ sudo pip3 install mycli
``` ```
### Windows ### Windows
@ -201,7 +220,7 @@ Thanks to [PyMysql](https://github.com/PyMySQL/PyMySQL) for a pure python adapte
### Compatibility ### Compatibility
Mycli is tested on macOS and Linux. Mycli is tested on macOS and Linux, and requires Python 3.7 or better.
**Mycli is not tested on Windows**, but the libraries used in this app are Windows-compatible. **Mycli is not tested on Windows**, but the libraries used in this app are Windows-compatible.
This means it should work without any modifications. If you're unable to run it This means it should work without any modifications. If you're unable to run it

View file

@ -1,3 +1,36 @@
1.26.1 (2022/09/01)
===
Bug Fixes:
----------
* Require Python 3.7 in `setup.py`
1.26.0 (2022/09/01)
===================
Features:
---------
* Add `--ssl` flag to enable ssl/tls.
* Add `pager` option to `~/.myclirc`, for instance `pager = 'pspg --csv'` (Thanks: [BuonOmo])
* Add prettify/unprettify keybindings to format the current statement using `sqlglot`.
Internal:
---------
* Pin `cryptography` to suppress `paramiko` warning, helping CI complete and presumably affecting some users.
* Upgrade some dev requirements
* Change tests to always use databases prefixed with 'mycli_' for better security
Bug Fixes:
----------
* Support for some MySQL compatible databases, which may not implement connection_id().
* Fix the status command to work with missing 'Flush_commands' (mariadb)
* Ignore the user of the system [myslqd] config.
1.25.0 (2022/04/02) 1.25.0 (2022/04/02)
=================== ===================
@ -18,6 +51,7 @@ Bug Fixes:
* Change in main.py - Replace the `click.get_terminal_size()` with `shutil.get_terminal_size()` * Change in main.py - Replace the `click.get_terminal_size()` with `shutil.get_terminal_size()`
1.24.3 (2022/01/20) 1.24.3 (2022/01/20)
=================== ===================
@ -872,6 +906,7 @@ Bug Fixes:
[Amjith Ramanujam]: https://blog.amjith.com [Amjith Ramanujam]: https://blog.amjith.com
[Artem Bezsmertnyi]: https://github.com/mrdeathless [Artem Bezsmertnyi]: https://github.com/mrdeathless
[BuonOmo]: https://github.com/BuonOmo
[Carlos Afonso]: https://github.com/afonsocarlos [Carlos Afonso]: https://github.com/afonsocarlos
[Casper Langemeijer]: https://github.com/langemeijer [Casper Langemeijer]: https://github.com/langemeijer
[Daniel West]: http://github.com/danieljwest [Daniel West]: http://github.com/danieljwest

65
doc/key_bindings.rst Normal file
View file

@ -0,0 +1,65 @@
*************
Key Bindings:
*************
Most key bindings are simply inherited from `prompt-toolkit <https://python-prompt-toolkit.readthedocs.io/en/master/index.html>`_ .
The following key bindings are special to mycli:
###
F2
###
Enable/Disable SmartCompletion Mode.
###
F3
###
Enable/Disable Multiline Mode.
###
F4
###
Toggle between Vi and Emacs mode.
###
Tab
###
Force autocompletion at cursor.
#######
C-space
#######
Initialize autocompletion at cursor.
If the autocompletion menu is not showing, display it with the appropriate completions for the context.
If the menu is showing, select the next completion.
#########
ESC Enter
#########
Introduce a line break in multi-line mode, or dispatch the command in single-line mode.
The sequence ESC-Enter is often sent by Alt-Enter.
#################################
C-x p (Emacs-mode) or > (Vi-mode)
#################################
Prettify and indent current statement, usually into multiple lines.
Only accepts buffers containing single SQL statements.
#################################
C-x u (Emacs-mode) or < (Vi-mode)
#################################
Unprettify and dedent current statement, usually into one line.
Only accepts buffers containing single SQL statements.

View file

@ -24,10 +24,12 @@ Contributors:
* Artem Bezsmertnyi * Artem Bezsmertnyi
* bitkeen * bitkeen
* bjarnagin * bjarnagin
* BuonOmo
* caitinggui * caitinggui
* Carlos Afonso * Carlos Afonso
* Casper Langemeijer * Casper Langemeijer
* chainkite * chainkite
* Claude Becker
* Colin Caine * Colin Caine
* cxbig * cxbig
* Daniel Black * Daniel Black
@ -38,6 +40,7 @@ Contributors:
* Georgy Frolov * Georgy Frolov
* Heath Naylor * Heath Naylor
* Huachao Mao * Huachao Mao
* Ishaan Bhimwal
* Jakub Boukal * Jakub Boukal
* jbruno * jbruno
* Jerome Provensal * Jerome Provensal
@ -81,6 +84,7 @@ Contributors:
* xeron * xeron
* Yang Zou * Yang Zou
* Yasuhiro Matsumoto * Yasuhiro Matsumoto
* Yuanchun Shang
* Zach DeCook * Zach DeCook
* Zane C. Bowers-Hadley * Zane C. Bowers-Hadley
* zer09 * zer09
@ -88,6 +92,8 @@ Contributors:
* Zhidong * Zhidong
* Zhongyang Guan * Zhongyang Guan
* Arvind Mishra * Arvind Mishra
* Kevin Schmeichel
* Mel Dafert
Created by: Created by:
----------- -----------

View file

@ -1 +1 @@
__version__ = '1.25.0' __version__ = '1.26.1'

View file

@ -30,6 +30,11 @@ def create_toolbar_tokens_func(mycli, show_fish_help):
'Vi-mode ({})'.format(_get_vi_mode()) 'Vi-mode ({})'.format(_get_vi_mode())
)) ))
if mycli.toolbar_error_message:
result.append(
('class:bottom-toolbar', ' ' + mycli.toolbar_error_message))
mycli.toolbar_error_message = None
if show_fish_help(): if show_fish_help():
result.append( result.append(
('class:bottom-toolbar', ' Right-arrow to complete suggestion')) ('class:bottom-toolbar', ' Right-arrow to complete suggestion'))

View file

@ -47,7 +47,7 @@ class CompletionRefresher(object):
def _bg_refresh(self, sqlexecute, callbacks, completer_options): def _bg_refresh(self, sqlexecute, callbacks, completer_options):
completer = SQLCompleter(**completer_options) completer = SQLCompleter(**completer_options)
# Create a new pgexecute method to popoulate the completions. # Create a new pgexecute method to populate the completions.
e = sqlexecute e = sqlexecute
executor = SQLExecute(e.dbname, e.user, e.password, e.host, e.port, executor = SQLExecute(e.dbname, e.user, e.password, e.host, e.port,
e.socket, e.charset, e.local_infile, e.ssl, e.socket, e.charset, e.local_infile, e.ssl,

View file

@ -1,6 +1,6 @@
import logging import logging
from prompt_toolkit.enums import EditingMode from prompt_toolkit.enums import EditingMode
from prompt_toolkit.filters import completion_is_selected from prompt_toolkit.filters import completion_is_selected, emacs_mode, vi_mode
from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding import KeyBindings
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -61,6 +61,48 @@ def mycli_bindings(mycli):
else: else:
b.start_completion(select_first=False) b.start_completion(select_first=False)
@kb.add('>', filter=vi_mode)
@kb.add('c-x', 'p', filter=emacs_mode)
def _(event):
"""
Prettify and indent current statement, usually into multiple lines.
Only accepts buffers containing single SQL statements.
"""
_logger.debug('Detected <C-x p>/> key.')
b = event.app.current_buffer
cursorpos_relative = b.cursor_position / len(b.text)
pretty_text = mycli.handle_prettify_binding(b.text)
if len(pretty_text) > 0:
b.text = pretty_text
cursorpos_abs = int(round(cursorpos_relative * len(b.text)))
while 0 < cursorpos_abs < len(b.text) \
and b.text[cursorpos_abs] in (' ', '\n'):
cursorpos_abs -= 1
b.cursor_position = min(cursorpos_abs, len(b.text))
@kb.add('<', filter=vi_mode)
@kb.add('c-x', 'u', filter=emacs_mode)
def _(event):
"""
Unprettify and dedent current statement, usually into one line.
Only accepts buffers containing single SQL statements.
"""
_logger.debug('Detected <C-x u>/< key.')
b = event.app.current_buffer
cursorpos_relative = b.cursor_position / len(b.text)
unpretty_text = mycli.handle_unprettify_binding(b.text)
if len(unpretty_text) > 0:
b.text = unpretty_text
cursorpos_abs = int(round(cursorpos_relative * len(b.text)))
while 0 < cursorpos_abs < len(b.text) \
and b.text[cursorpos_abs] in (' ', '\n'):
cursorpos_abs -= 1
b.cursor_position = min(cursorpos_abs, len(b.text))
@kb.add('enter', filter=completion_is_selected) @kb.add('enter', filter=completion_is_selected)
def _(event): def _(event):
"""Makes the enter key work as the tab key only when showing the menu. """Makes the enter key work as the tab key only when showing the menu.

View file

@ -24,6 +24,7 @@ from cli_helpers.tabular_output import preprocessors
from cli_helpers.utils import strip_ansi from cli_helpers.utils import strip_ansi
import click import click
import sqlparse import sqlparse
import sqlglot
from mycli.packages.parseutils import is_dropping_database, is_destructive from mycli.packages.parseutils import is_dropping_database, is_destructive
from prompt_toolkit.completion import DynamicCompleter from prompt_toolkit.completion import DynamicCompleter
from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
@ -123,6 +124,7 @@ class MyCli(object):
self.logfile = logfile self.logfile = logfile
self.defaults_suffix = defaults_suffix self.defaults_suffix = defaults_suffix
self.login_path = login_path self.login_path = login_path
self.toolbar_error_message = None
# self.cnf_files is a class variable that stores the list of mysql # self.cnf_files is a class variable that stores the list of mysql
# config files to read in at launch. # config files to read in at launch.
@ -341,6 +343,7 @@ class MyCli(object):
'mysqld': { 'mysqld': {
'socket': 'default_socket', 'socket': 'default_socket',
'port': 'default_port', 'port': 'default_port',
'user': 'default_user',
}, },
} }
@ -447,7 +450,7 @@ class MyCli(object):
if not any(v for v in ssl.values()): if not any(v for v in ssl.values()):
ssl = None ssl = None
# if the passwd is not specfied try to set it using the password_file option # if the passwd is not specified try to set it using the password_file option
password_from_file = self.get_password_from_file(password_file) password_from_file = self.get_password_from_file(password_file)
passwd = passwd or password_from_file passwd = passwd or password_from_file
@ -581,6 +584,34 @@ class MyCli(object):
return True return True
return False return False
def handle_prettify_binding(self, text):
try:
statements = sqlglot.parse(text, read='mysql')
except Exception as e:
statements = []
if len(statements) == 1:
pretty_text = statements[0].sql(pretty=True, pad=4, dialect='mysql')
else:
pretty_text = ''
self.toolbar_error_message = 'Prettify failed to parse statement'
if len(pretty_text) > 0:
pretty_text = pretty_text + ';'
return pretty_text
def handle_unprettify_binding(self, text):
try:
statements = sqlglot.parse(text, read='mysql')
except Exception as e:
statements = []
if len(statements) == 1:
unpretty_text = statements[0].sql(pretty=False, dialect='mysql')
else:
unpretty_text = ''
self.toolbar_error_message = 'Unprettify failed to parse statement'
if len(unpretty_text) > 0:
unpretty_text = unpretty_text + ';'
return unpretty_text
def run_cli(self): def run_cli(self):
iterations = 0 iterations = 0
sqlexecute = self.sqlexecute sqlexecute = self.sqlexecute
@ -723,7 +754,7 @@ class MyCli(object):
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
if self.beep_after_seconds > 0 and t >= self.beep_after_seconds: if self.beep_after_seconds > 0 and t >= self.beep_after_seconds:
self.echo('\a', err=True, nl=False) self.bell()
if special.is_timing_enabled(): if special.is_timing_enabled():
self.echo('Time: %0.03fs' % t) self.echo('Time: %0.03fs' % t)
except KeyboardInterrupt: except KeyboardInterrupt:
@ -739,19 +770,23 @@ class MyCli(object):
except KeyboardInterrupt: except KeyboardInterrupt:
# get last connection id # get last connection id
connection_id_to_kill = sqlexecute.connection_id connection_id_to_kill = sqlexecute.connection_id
logger.debug("connection id to kill: %r", connection_id_to_kill) # some mysql compatible databases may not implemente connection_id()
# Restart connection to the database if connection_id_to_kill > 0:
sqlexecute.connect() logger.debug("connection id to kill: %r", connection_id_to_kill)
try: # Restart connection to the database
for title, cur, headers, status in sqlexecute.run('kill %s' % connection_id_to_kill): sqlexecute.connect()
status_str = str(status).lower() try:
if status_str.find('ok') > -1: for title, cur, headers, status in sqlexecute.run('kill %s' % connection_id_to_kill):
logger.debug("cancelled query, connection id: %r, sql: %r", status_str = str(status).lower()
connection_id_to_kill, text) if status_str.find('ok') > -1:
self.echo("cancelled query", err=True, fg='red') logger.debug("cancelled query, connection id: %r, sql: %r",
except Exception as e: connection_id_to_kill, text)
self.echo('Encountered error while cancelling query: {}'.format(e), self.echo("cancelled query", err=True, fg='red')
err=True, fg='red') except Exception as e:
self.echo('Encountered error while cancelling query: {}'.format(e),
err=True, fg='red')
else:
logger.debug("Did not get a connection id, skip cancelling query")
except NotImplementedError: except NotImplementedError:
self.echo('Not Yet Implemented.', fg="yellow") self.echo('Not Yet Implemented.', fg="yellow")
except OperationalError as e: except OperationalError as e:
@ -860,6 +895,11 @@ class MyCli(object):
self.log_output(s) self.log_output(s)
click.secho(s, **kwargs) click.secho(s, **kwargs)
def bell(self):
"""Print a bell on the stderr.
"""
click.secho('\a', err=True, nl=False)
def get_output_margin(self, status=None): def get_output_margin(self, status=None):
"""Get the output margin (number of rows for the prompt, footer and """Get the output margin (number of rows for the prompt, footer and
timing message.""" timing message."""
@ -933,8 +973,9 @@ class MyCli(object):
os.environ['LESS'] = '-RXF' os.environ['LESS'] = '-RXF'
cnf = self.read_my_cnf_files(self.cnf_files, ['pager', 'skip-pager']) cnf = self.read_my_cnf_files(self.cnf_files, ['pager', 'skip-pager'])
if cnf['pager']: cnf_pager = cnf['pager'] or self.config['main']['pager']
special.set_pager(cnf['pager']) if cnf_pager:
special.set_pager(cnf_pager)
self.explicit_pager = True self.explicit_pager = True
else: else:
self.explicit_pager = False self.explicit_pager = False
@ -1084,6 +1125,8 @@ class MyCli(object):
@click.option('--ssh-config-path', help='Path to ssh configuration.', @click.option('--ssh-config-path', help='Path to ssh configuration.',
default=os.path.expanduser('~') + '/.ssh/config') default=os.path.expanduser('~') + '/.ssh/config')
@click.option('--ssh-config-host', help='Host to connect to ssh server reading from ssh configuration.') @click.option('--ssh-config-host', help='Host to connect to ssh server reading from ssh configuration.')
@click.option('--ssl', 'ssl_enable', is_flag=True,
help='Enable SSL for connection (automatically enabled with other flags).')
@click.option('--ssl-ca', help='CA file in PEM format.', @click.option('--ssl-ca', help='CA file in PEM format.',
type=click.Path(exists=True)) type=click.Path(exists=True))
@click.option('--ssl-capath', help='CA directory.') @click.option('--ssl-capath', help='CA directory.')
@ -1142,7 +1185,7 @@ class MyCli(object):
def cli(database, user, host, port, socket, password, dbname, def cli(database, user, host, port, socket, password, dbname,
version, verbose, prompt, logfile, defaults_group_suffix, version, verbose, prompt, logfile, defaults_group_suffix,
defaults_file, login_path, auto_vertical_output, local_infile, defaults_file, login_path, auto_vertical_output, local_infile,
ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher, ssl_enable, ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher,
ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn, ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn,
list_dsn, ssh_user, ssh_host, ssh_port, ssh_password, list_dsn, ssh_user, ssh_host, ssh_port, ssh_password,
ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host, ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host,
@ -1197,6 +1240,7 @@ def cli(database, user, host, port, socket, password, dbname,
database = dbname or database database = dbname or database
ssl = { ssl = {
'enable': ssl_enable,
'ca': ssl_ca and os.path.expanduser(ssl_ca), 'ca': ssl_ca and os.path.expanduser(ssl_ca),
'cert': ssl_cert and os.path.expanduser(ssl_cert), 'cert': ssl_cert and os.path.expanduser(ssl_cert),
'key': ssl_key and os.path.expanduser(ssl_key), 'key': ssl_key and os.path.expanduser(ssl_key),

View file

@ -27,7 +27,7 @@ log_level = INFO
# line below. # line below.
# audit_log = ~/.mycli-audit.log # audit_log = ~/.mycli-audit.log
# Timing of sql statments and table rendering. # Timing of sql statements and table rendering.
timing = True timing = True
# Beep after long-running queries are completed; 0 to disable. # Beep after long-running queries are completed; 0 to disable.
@ -66,7 +66,7 @@ wider_completion_menu = False
# \R - The current time, in 24-hour military time (0-23) # \R - The current time, in 24-hour military time (0-23)
# \r - The current time, standard 12-hour time (1-12) # \r - The current time, standard 12-hour time (1-12)
# \s - Seconds of the current time # \s - Seconds of the current time
# \t - Product type (Percona, MySQL, MariaDB) # \t - Product type (Percona, MySQL, MariaDB, TiDB)
# \A - DSN alias name (from the [alias_dsn] section) # \A - DSN alias name (from the [alias_dsn] section)
# \u - Username # \u - Username
# \x1b[...m - insert ANSI escape sequence # \x1b[...m - insert ANSI escape sequence
@ -89,6 +89,9 @@ keyword_casing = auto
# disabled pager on startup # disabled pager on startup
enable_pager = True enable_pager = True
# Choose a specific pager
pager = 'less'
# Custom colors for the completion menu, toolbar, etc. # Custom colors for the completion menu, toolbar, etc.
[colors] [colors]
completion-menu.completion.current = 'bg:#ffffff #000000' completion-menu.completion.current = 'bg:#ffffff #000000'

View file

@ -129,6 +129,8 @@ def suggest_based_on_last_token(token, text_before_cursor, full_text, identifier
prev_keyword, text_before_cursor = find_prev_keyword(text_before_cursor) prev_keyword, text_before_cursor = find_prev_keyword(text_before_cursor)
return suggest_based_on_last_token(prev_keyword, text_before_cursor, return suggest_based_on_last_token(prev_keyword, text_before_cursor,
full_text, identifier) full_text, identifier)
elif token is None:
return [{'type': 'keyword'}]
else: else:
token_v = token.value.lower() token_v = token.value.lower()

View file

@ -143,7 +143,7 @@ def extract_table_identifiers(token_stream):
# extract_tables is inspired from examples in the sqlparse lib. # extract_tables is inspired from examples in the sqlparse lib.
def extract_tables(sql): def extract_tables(sql):
"""Extract the table names from an SQL statment. """Extract the table names from an SQL statement.
Returns a list of (schema, table, alias) tuples Returns a list of (schema, table, alias) tuples

View file

@ -34,6 +34,7 @@ def list_tables(cur, arg=None, arg_type=PARSED_QUERY, verbose=False):
return [(None, tables, headers, status)] return [(None, tables, headers, status)]
@special_command('\\l', '\\l', 'List databases.', arg_type=RAW_QUERY, case_sensitive=True) @special_command('\\l', '\\l', 'List databases.', arg_type=RAW_QUERY, case_sensitive=True)
def list_databases(cur, **_): def list_databases(cur, **_):
query = 'SHOW DATABASES' query = 'SHOW DATABASES'
@ -45,6 +46,7 @@ def list_databases(cur, **_):
else: else:
return [(None, None, None, '')] return [(None, None, None, '')]
@special_command('status', '\\s', 'Get status information from the server.', @special_command('status', '\\s', 'Get status information from the server.',
arg_type=RAW_QUERY, aliases=('\\s', ), case_sensitive=True) arg_type=RAW_QUERY, aliases=('\\s', ), case_sensitive=True)
def status(cur, **_): def status(cur, **_):
@ -146,7 +148,8 @@ def status(cur, **_):
stats.append('Queries: {0}'.format(status['Queries'])) stats.append('Queries: {0}'.format(status['Queries']))
stats.append('Slow queries: {0}'.format(status['Slow_queries'])) stats.append('Slow queries: {0}'.format(status['Slow_queries']))
stats.append('Opens: {0}'.format(status['Opened_tables'])) stats.append('Opens: {0}'.format(status['Opened_tables']))
stats.append('Flush tables: {0}'.format(status['Flush_commands'])) if 'Flush_commands' in status:
stats.append('Flush tables: {0}'.format(status['Flush_commands']))
stats.append('Open tables: {0}'.format(status['Open_tables'])) stats.append('Open tables: {0}'.format(status['Open_tables']))
if 'Queries' in status: if 'Queries' in status:
queries_per_second = int(status['Queries']) / int(status['Uptime']) queries_per_second = int(status['Queries']) / int(status['Uptime'])

View file

@ -28,6 +28,7 @@ class ServerSpecies(enum.Enum):
MySQL = 'MySQL' MySQL = 'MySQL'
MariaDB = 'MariaDB' MariaDB = 'MariaDB'
Percona = 'Percona' Percona = 'Percona'
TiDB = 'TiDB'
Unknown = 'MySQL' Unknown = 'MySQL'
@ -55,6 +56,7 @@ class ServerInfo:
re_species = ( re_species = (
(r'(?P<version>[0-9\.]+)-MariaDB', ServerSpecies.MariaDB), (r'(?P<version>[0-9\.]+)-MariaDB', ServerSpecies.MariaDB),
(r'(?P<version>[0-9\.]+)[a-z0-9]*-TiDB', ServerSpecies.TiDB),
(r'(?P<version>[0-9\.]+)[a-z0-9]*-(?P<comment>[0-9]+$)', (r'(?P<version>[0-9\.]+)[a-z0-9]*-(?P<comment>[0-9]+$)',
ServerSpecies.Percona), ServerSpecies.Percona),
(r'(?P<version>[0-9\.]+)[a-z0-9]*-(?P<comment>[A-Za-z0-9_]+)', (r'(?P<version>[0-9\.]+)[a-z0-9]*-(?P<comment>[A-Za-z0-9_]+)',
@ -338,10 +340,16 @@ class SQLExecute(object):
def reset_connection_id(self): def reset_connection_id(self):
# Remember current connection id # Remember current connection id
_logger.debug('Get current connection id') _logger.debug('Get current connection id')
res = self.run('select connection_id()') try:
for title, cur, headers, status in res: res = self.run('select connection_id()')
self.connection_id = cur.fetchone()[0] for title, cur, headers, status in res:
_logger.debug('Current connection id: %s', self.connection_id) self.connection_id = cur.fetchone()[0]
except Exception as e:
# See #1054
self.connection_id = -1
_logger.error('Failed to get connection id: %s', e)
else:
_logger.debug('Current connection id: %s', self.connection_id)
def change_db(self, db): def change_db(self, db):
self.conn.select_db(db) self.conn.select_db(db)

View file

@ -10,4 +10,8 @@ autopep8==1.3.3
colorama>=0.4.1 colorama>=0.4.1
git+https://github.com/hayd/pep8radius.git # --error-status option not released git+https://github.com/hayd/pep8radius.git # --error-status option not released
click>=7.0 click>=7.0
paramiko==2.7.1 paramiko==2.11.0
pyperclip>=1.8.1
importlib_resources>=5.0.0
pyaes>=1.6.1
sqlglot>=5.1.3

View file

@ -18,12 +18,15 @@ description = 'CLI for MySQL Database. With auto-completion and syntax highlight
install_requirements = [ install_requirements = [
'click >= 7.0', 'click >= 7.0',
'cryptography >= 1.0.0', # Temporary to suppress paramiko Blowfish warning which breaks CI.
# Pinning cryptography should not be needed after paramiko 2.11.0.
'cryptography == 36.0.2',
# 'Pygments>=1.6,<=2.11.1', # 'Pygments>=1.6,<=2.11.1',
'Pygments>=1.6', 'Pygments>=1.6',
'prompt_toolkit>=3.0.6,<4.0.0', 'prompt_toolkit>=3.0.6,<4.0.0',
'PyMySQL >= 0.9.2', 'PyMySQL >= 0.9.2',
'sqlparse>=0.3.0,<0.5.0', 'sqlparse>=0.3.0,<0.5.0',
'sqlglot>=5.1.3',
'configobj >= 5.0.5', 'configobj >= 5.0.5',
'cli_helpers[styles] >= 2.2.1', 'cli_helpers[styles] >= 2.2.1',
'pyperclip >= 1.8.1', 'pyperclip >= 1.8.1',
@ -101,16 +104,17 @@ setup(
'console_scripts': ['mycli = mycli.main:cli'], 'console_scripts': ['mycli = mycli.main:cli'],
}, },
cmdclass={'lint': lint, 'test': test}, cmdclass={'lint': lint, 'test': test},
python_requires=">=3.6", python_requires=">=3.7",
classifiers=[ classifiers=[
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: Unix', 'Operating System :: Unix',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: SQL', 'Programming Language :: SQL',
'Topic :: Database', 'Topic :: Database',
'Topic :: Database :: Front-Ends', 'Topic :: Database :: Front-Ends',

View file

@ -6,8 +6,8 @@ import mycli.sqlexecute
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def connection(): def connection():
create_db('_test_db') create_db('mycli_test_db')
connection = db_connection('_test_db') connection = db_connection('mycli_test_db')
yield connection yield connection
connection.close() connection.close()
@ -22,7 +22,7 @@ def cursor(connection):
@pytest.fixture @pytest.fixture
def executor(connection): def executor(connection):
return mycli.sqlexecute.SQLExecute( return mycli.sqlexecute.SQLExecute(
database='_test_db', user=USER, database='mycli_test_db', user=USER,
host=HOST, password=PASSWORD, port=PORT, socket=None, charset=CHARSET, host=HOST, password=PASSWORD, port=PORT, socket=None, charset=CHARSET,
local_infile=False, ssl=None, ssh_user=SSH_USER, ssh_host=SSH_HOST, local_infile=False, ssl=None, ssh_user=SSH_USER, ssh_host=SSH_HOST,
ssh_port=SSH_PORT, ssh_password=None, ssh_key_filename=None ssh_port=SSH_PORT, ssh_password=None, ssh_key_filename=None

View file

@ -542,7 +542,14 @@ def test_favorite_name_suggestion(expression):
suggestions = suggest_type(expression, expression) suggestions = suggest_type(expression, expression)
assert suggestions == [{'type': 'favoritequery'}] assert suggestions == [{'type': 'favoritequery'}]
def test_order_by(): def test_order_by():
text = 'select * from foo order by ' text = 'select * from foo order by '
suggestions = suggest_type(text, text) suggestions = suggest_type(text, text)
assert suggestions == [{'tables': [(None, 'foo', None)], 'type': 'column'}] assert suggestions == [{'tables': [(None, 'foo', None)], 'type': 'column'}]
def test_quoted_where():
text = "'where i=';"
suggestions = suggest_type(text, text)
assert suggestions == [{'type': 'keyword'}]

View file

@ -25,7 +25,7 @@ os.environ['MYSQL_TEST_LOGIN_FILE'] = login_path_file
CLI_ARGS = ['--user', USER, '--host', HOST, '--port', PORT, CLI_ARGS = ['--user', USER, '--host', HOST, '--port', PORT,
'--password', PASSWORD, '--myclirc', default_config_file, '--password', PASSWORD, '--myclirc', default_config_file,
'--defaults-file', default_config_file, '--defaults-file', default_config_file,
'_test_db'] 'mycli_test_db']
@dbtest @dbtest
@ -283,6 +283,20 @@ def test_list_dsn():
assert result.output == "test : mysql://test/test\n" assert result.output == "test : mysql://test/test\n"
def test_prettify_statement():
statement = 'SELECT 1'
m = MyCli()
pretty_statement = m.handle_prettify_binding(statement)
assert pretty_statement == 'SELECT\n 1;'
def test_unprettify_statement():
statement = 'SELECT\n 1'
m = MyCli()
unpretty_statement = m.handle_unprettify_binding(statement)
assert unpretty_statement == 'SELECT 1;'
def test_list_ssh_config(): def test_list_ssh_config():
runner = CliRunner() runner = CliRunner()
with NamedTemporaryFile(mode="w") as ssh_config: with NamedTemporaryFile(mode="w") as ssh_config:
@ -305,19 +319,25 @@ def test_dsn(monkeypatch):
# Setup classes to mock mycli.main.MyCli # Setup classes to mock mycli.main.MyCli
class Formatter: class Formatter:
format_name = None format_name = None
class Logger: class Logger:
def debug(self, *args, **args_dict): def debug(self, *args, **args_dict):
pass pass
def warning(self, *args, **args_dict): def warning(self, *args, **args_dict):
pass pass
class MockMyCli: class MockMyCli:
config = {'alias_dsn': {}} config = {'alias_dsn': {}}
def __init__(self, **args): def __init__(self, **args):
self.logger = Logger() self.logger = Logger()
self.destructive_warning = False self.destructive_warning = False
self.formatter = Formatter() self.formatter = Formatter()
def connect(self, **args): def connect(self, **args):
MockMyCli.connect_args = args MockMyCli.connect_args = args
def run_query(self, query, new_line=True): def run_query(self, query, new_line=True):
pass pass

View file

@ -71,7 +71,7 @@ def test_table_and_columns_query(executor):
@dbtest @dbtest
def test_database_list(executor): def test_database_list(executor):
databases = executor.databases() databases = executor.databases()
assert '_test_db' in databases assert 'mycli_test_db' in databases
@dbtest @dbtest
@ -276,6 +276,7 @@ def test_multiple_results(executor):
@pytest.mark.parametrize( @pytest.mark.parametrize(
'version_string, species, parsed_version_string, version', 'version_string, species, parsed_version_string, version',
( (
('5.7.25-TiDB-v6.1.0','TiDB', '5.7.25', 50725),
('5.7.32-35', 'Percona', '5.7.32', 50732), ('5.7.32-35', 'Percona', '5.7.32', 50732),
('5.7.32-0ubuntu0.18.04.1', 'MySQL', '5.7.32', 50732), ('5.7.32-0ubuntu0.18.04.1', 'MySQL', '5.7.32', 50732),
('10.5.8-MariaDB-1:10.5.8+maria~focal', 'MariaDB', '10.5.8', 100508), ('10.5.8-MariaDB-1:10.5.8+maria~focal', 'MariaDB', '10.5.8', 100508),

View file

@ -41,8 +41,8 @@ dbtest = pytest.mark.skipif(
def create_db(dbname): def create_db(dbname):
with db_connection().cursor() as cur: with db_connection().cursor() as cur:
try: try:
cur.execute('''DROP DATABASE IF EXISTS _test_db''') cur.execute('''DROP DATABASE IF EXISTS mycli_test_db''')
cur.execute('''CREATE DATABASE _test_db''') cur.execute('''CREATE DATABASE mycli_test_db''')
except: except:
pass pass