Merging upstream version 1.26.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
79468558b6
commit
c35ab76feb
23 changed files with 328 additions and 52 deletions
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
|
@ -9,16 +9,16 @@ jobs:
|
|||
linux:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10']
|
||||
include:
|
||||
- python-version: 3.6
|
||||
os: ubuntu-18.04 # MySQL 5.7.32
|
||||
- python-version: 3.7
|
||||
os: ubuntu-18.04 # MySQL 5.7.32
|
||||
- python-version: 3.8
|
||||
os: ubuntu-18.04 # MySQL 5.7.32
|
||||
- python-version: 3.9
|
||||
- python-version: '3.7'
|
||||
os: ubuntu-18.04 # MySQL 5.7.32
|
||||
- python-version: '3.8'
|
||||
os: ubuntu-18.04 # MySQL 5.7.32
|
||||
- python-version: '3.9'
|
||||
os: ubuntu-20.04 # MySQL 8.0.22
|
||||
- python-version: '3.10'
|
||||
os: ubuntu-22.04 # MySQL 8.0.28
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
|
|
@ -95,7 +95,7 @@ credentials to use by setting the applicable environment variables:
|
|||
|
||||
```bash
|
||||
$ export PYTEST_HOST=localhost
|
||||
$ export PYTEST_USER=user
|
||||
$ export PYTEST_USER=mycli
|
||||
$ export PYTEST_PASSWORD=myclirocks
|
||||
$ export PYTEST_PORT=3306
|
||||
$ export PYTEST_CHARSET=utf8
|
||||
|
@ -104,6 +104,14 @@ $ export PYTEST_CHARSET=utf8
|
|||
The default values are `localhost`, `root`, no password, `3306`, and `utf8`.
|
||||
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
|
||||
|
||||
|
|
25
README.md
25
README.md
|
@ -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
|
||||
configuration.
|
||||
|
||||
--ssl Enable SSL for connection (automatically
|
||||
enabled with other flags).
|
||||
--ssl-ca PATH CA file in PEM format.
|
||||
--ssl-capath TEXT CA directory.
|
||||
--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).
|
||||
* Pretty prints tabular data (with colors!)
|
||||
* Support for SSL connections
|
||||
* Some features are only exposed as [key bindings](doc/key_bindings.rst)
|
||||
|
||||
Contributions:
|
||||
--------------
|
||||
|
@ -151,6 +154,22 @@ Twitter: [@amjithr](http://twitter.com/amjithr)
|
|||
|
||||
## 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 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:
|
||||
|
||||
```
|
||||
$ sudo yum install python-pip
|
||||
$ sudo yum install python3-pip
|
||||
```
|
||||
|
||||
Once that is installed, you can install mycli as follows:
|
||||
|
||||
```
|
||||
$ sudo pip install mycli
|
||||
$ sudo pip3 install mycli
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
@ -201,7 +220,7 @@ Thanks to [PyMysql](https://github.com/PyMySQL/PyMySQL) for a pure python adapte
|
|||
|
||||
### 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.
|
||||
This means it should work without any modifications. If you're unable to run it
|
||||
|
|
35
changelog.md
35
changelog.md
|
@ -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)
|
||||
===================
|
||||
|
||||
|
@ -18,6 +51,7 @@ Bug Fixes:
|
|||
* Change in main.py - Replace the `click.get_terminal_size()` with `shutil.get_terminal_size()`
|
||||
|
||||
|
||||
|
||||
1.24.3 (2022/01/20)
|
||||
===================
|
||||
|
||||
|
@ -872,6 +906,7 @@ Bug Fixes:
|
|||
|
||||
[Amjith Ramanujam]: https://blog.amjith.com
|
||||
[Artem Bezsmertnyi]: https://github.com/mrdeathless
|
||||
[BuonOmo]: https://github.com/BuonOmo
|
||||
[Carlos Afonso]: https://github.com/afonsocarlos
|
||||
[Casper Langemeijer]: https://github.com/langemeijer
|
||||
[Daniel West]: http://github.com/danieljwest
|
||||
|
|
65
doc/key_bindings.rst
Normal file
65
doc/key_bindings.rst
Normal 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.
|
|
@ -24,10 +24,12 @@ Contributors:
|
|||
* Artem Bezsmertnyi
|
||||
* bitkeen
|
||||
* bjarnagin
|
||||
* BuonOmo
|
||||
* caitinggui
|
||||
* Carlos Afonso
|
||||
* Casper Langemeijer
|
||||
* chainkite
|
||||
* Claude Becker
|
||||
* Colin Caine
|
||||
* cxbig
|
||||
* Daniel Black
|
||||
|
@ -38,6 +40,7 @@ Contributors:
|
|||
* Georgy Frolov
|
||||
* Heath Naylor
|
||||
* Huachao Mao
|
||||
* Ishaan Bhimwal
|
||||
* Jakub Boukal
|
||||
* jbruno
|
||||
* Jerome Provensal
|
||||
|
@ -81,6 +84,7 @@ Contributors:
|
|||
* xeron
|
||||
* Yang Zou
|
||||
* Yasuhiro Matsumoto
|
||||
* Yuanchun Shang
|
||||
* Zach DeCook
|
||||
* Zane C. Bowers-Hadley
|
||||
* zer09
|
||||
|
@ -88,6 +92,8 @@ Contributors:
|
|||
* Zhidong
|
||||
* Zhongyang Guan
|
||||
* Arvind Mishra
|
||||
* Kevin Schmeichel
|
||||
* Mel Dafert
|
||||
|
||||
Created by:
|
||||
-----------
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = '1.25.0'
|
||||
__version__ = '1.26.1'
|
||||
|
|
|
@ -30,6 +30,11 @@ def create_toolbar_tokens_func(mycli, show_fish_help):
|
|||
'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():
|
||||
result.append(
|
||||
('class:bottom-toolbar', ' Right-arrow to complete suggestion'))
|
||||
|
|
|
@ -47,7 +47,7 @@ class CompletionRefresher(object):
|
|||
def _bg_refresh(self, sqlexecute, callbacks, 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
|
||||
executor = SQLExecute(e.dbname, e.user, e.password, e.host, e.port,
|
||||
e.socket, e.charset, e.local_infile, e.ssl,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
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
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
@ -61,6 +61,48 @@ def mycli_bindings(mycli):
|
|||
else:
|
||||
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)
|
||||
def _(event):
|
||||
"""Makes the enter key work as the tab key only when showing the menu.
|
||||
|
|
|
@ -24,6 +24,7 @@ from cli_helpers.tabular_output import preprocessors
|
|||
from cli_helpers.utils import strip_ansi
|
||||
import click
|
||||
import sqlparse
|
||||
import sqlglot
|
||||
from mycli.packages.parseutils import is_dropping_database, is_destructive
|
||||
from prompt_toolkit.completion import DynamicCompleter
|
||||
from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
|
||||
|
@ -123,6 +124,7 @@ class MyCli(object):
|
|||
self.logfile = logfile
|
||||
self.defaults_suffix = defaults_suffix
|
||||
self.login_path = login_path
|
||||
self.toolbar_error_message = None
|
||||
|
||||
# self.cnf_files is a class variable that stores the list of mysql
|
||||
# config files to read in at launch.
|
||||
|
@ -341,6 +343,7 @@ class MyCli(object):
|
|||
'mysqld': {
|
||||
'socket': 'default_socket',
|
||||
'port': 'default_port',
|
||||
'user': 'default_user',
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -447,7 +450,7 @@ class MyCli(object):
|
|||
if not any(v for v in ssl.values()):
|
||||
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)
|
||||
passwd = passwd or password_from_file
|
||||
|
||||
|
@ -581,6 +584,34 @@ class MyCli(object):
|
|||
return True
|
||||
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):
|
||||
iterations = 0
|
||||
sqlexecute = self.sqlexecute
|
||||
|
@ -723,7 +754,7 @@ class MyCli(object):
|
|||
except KeyboardInterrupt:
|
||||
pass
|
||||
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():
|
||||
self.echo('Time: %0.03fs' % t)
|
||||
except KeyboardInterrupt:
|
||||
|
@ -739,19 +770,23 @@ class MyCli(object):
|
|||
except KeyboardInterrupt:
|
||||
# get last connection id
|
||||
connection_id_to_kill = sqlexecute.connection_id
|
||||
logger.debug("connection id to kill: %r", connection_id_to_kill)
|
||||
# Restart connection to the database
|
||||
sqlexecute.connect()
|
||||
try:
|
||||
for title, cur, headers, status in sqlexecute.run('kill %s' % connection_id_to_kill):
|
||||
status_str = str(status).lower()
|
||||
if status_str.find('ok') > -1:
|
||||
logger.debug("cancelled query, connection id: %r, sql: %r",
|
||||
connection_id_to_kill, text)
|
||||
self.echo("cancelled query", err=True, fg='red')
|
||||
except Exception as e:
|
||||
self.echo('Encountered error while cancelling query: {}'.format(e),
|
||||
err=True, fg='red')
|
||||
# some mysql compatible databases may not implemente connection_id()
|
||||
if connection_id_to_kill > 0:
|
||||
logger.debug("connection id to kill: %r", connection_id_to_kill)
|
||||
# Restart connection to the database
|
||||
sqlexecute.connect()
|
||||
try:
|
||||
for title, cur, headers, status in sqlexecute.run('kill %s' % connection_id_to_kill):
|
||||
status_str = str(status).lower()
|
||||
if status_str.find('ok') > -1:
|
||||
logger.debug("cancelled query, connection id: %r, sql: %r",
|
||||
connection_id_to_kill, text)
|
||||
self.echo("cancelled query", 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:
|
||||
self.echo('Not Yet Implemented.', fg="yellow")
|
||||
except OperationalError as e:
|
||||
|
@ -860,6 +895,11 @@ class MyCli(object):
|
|||
self.log_output(s)
|
||||
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):
|
||||
"""Get the output margin (number of rows for the prompt, footer and
|
||||
timing message."""
|
||||
|
@ -933,8 +973,9 @@ class MyCli(object):
|
|||
os.environ['LESS'] = '-RXF'
|
||||
|
||||
cnf = self.read_my_cnf_files(self.cnf_files, ['pager', 'skip-pager'])
|
||||
if cnf['pager']:
|
||||
special.set_pager(cnf['pager'])
|
||||
cnf_pager = cnf['pager'] or self.config['main']['pager']
|
||||
if cnf_pager:
|
||||
special.set_pager(cnf_pager)
|
||||
self.explicit_pager = True
|
||||
else:
|
||||
self.explicit_pager = False
|
||||
|
@ -1084,6 +1125,8 @@ class MyCli(object):
|
|||
@click.option('--ssh-config-path', help='Path to ssh configuration.',
|
||||
default=os.path.expanduser('~') + '/.ssh/config')
|
||||
@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.',
|
||||
type=click.Path(exists=True))
|
||||
@click.option('--ssl-capath', help='CA directory.')
|
||||
|
@ -1142,7 +1185,7 @@ class MyCli(object):
|
|||
def cli(database, user, host, port, socket, password, dbname,
|
||||
version, verbose, prompt, logfile, defaults_group_suffix,
|
||||
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,
|
||||
list_dsn, ssh_user, ssh_host, ssh_port, ssh_password,
|
||||
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
|
||||
|
||||
ssl = {
|
||||
'enable': ssl_enable,
|
||||
'ca': ssl_ca and os.path.expanduser(ssl_ca),
|
||||
'cert': ssl_cert and os.path.expanduser(ssl_cert),
|
||||
'key': ssl_key and os.path.expanduser(ssl_key),
|
||||
|
|
|
@ -27,7 +27,7 @@ log_level = INFO
|
|||
# line below.
|
||||
# audit_log = ~/.mycli-audit.log
|
||||
|
||||
# Timing of sql statments and table rendering.
|
||||
# Timing of sql statements and table rendering.
|
||||
timing = True
|
||||
|
||||
# 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, standard 12-hour time (1-12)
|
||||
# \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)
|
||||
# \u - Username
|
||||
# \x1b[...m - insert ANSI escape sequence
|
||||
|
@ -89,6 +89,9 @@ keyword_casing = auto
|
|||
# disabled pager on startup
|
||||
enable_pager = True
|
||||
|
||||
# Choose a specific pager
|
||||
pager = 'less'
|
||||
|
||||
# Custom colors for the completion menu, toolbar, etc.
|
||||
[colors]
|
||||
completion-menu.completion.current = 'bg:#ffffff #000000'
|
||||
|
|
|
@ -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)
|
||||
return suggest_based_on_last_token(prev_keyword, text_before_cursor,
|
||||
full_text, identifier)
|
||||
elif token is None:
|
||||
return [{'type': 'keyword'}]
|
||||
else:
|
||||
token_v = token.value.lower()
|
||||
|
||||
|
|
|
@ -143,7 +143,7 @@ def extract_table_identifiers(token_stream):
|
|||
|
||||
# extract_tables is inspired from examples in the sqlparse lib.
|
||||
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
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ def list_tables(cur, arg=None, arg_type=PARSED_QUERY, verbose=False):
|
|||
|
||||
return [(None, tables, headers, status)]
|
||||
|
||||
|
||||
@special_command('\\l', '\\l', 'List databases.', arg_type=RAW_QUERY, case_sensitive=True)
|
||||
def list_databases(cur, **_):
|
||||
query = 'SHOW DATABASES'
|
||||
|
@ -45,6 +46,7 @@ def list_databases(cur, **_):
|
|||
else:
|
||||
return [(None, None, None, '')]
|
||||
|
||||
|
||||
@special_command('status', '\\s', 'Get status information from the server.',
|
||||
arg_type=RAW_QUERY, aliases=('\\s', ), case_sensitive=True)
|
||||
def status(cur, **_):
|
||||
|
@ -146,7 +148,8 @@ def status(cur, **_):
|
|||
stats.append('Queries: {0}'.format(status['Queries']))
|
||||
stats.append('Slow queries: {0}'.format(status['Slow_queries']))
|
||||
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']))
|
||||
if 'Queries' in status:
|
||||
queries_per_second = int(status['Queries']) / int(status['Uptime'])
|
||||
|
|
|
@ -28,6 +28,7 @@ class ServerSpecies(enum.Enum):
|
|||
MySQL = 'MySQL'
|
||||
MariaDB = 'MariaDB'
|
||||
Percona = 'Percona'
|
||||
TiDB = 'TiDB'
|
||||
Unknown = 'MySQL'
|
||||
|
||||
|
||||
|
@ -55,6 +56,7 @@ class ServerInfo:
|
|||
|
||||
re_species = (
|
||||
(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]+$)',
|
||||
ServerSpecies.Percona),
|
||||
(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):
|
||||
# Remember current connection id
|
||||
_logger.debug('Get current connection id')
|
||||
res = self.run('select connection_id()')
|
||||
for title, cur, headers, status in res:
|
||||
self.connection_id = cur.fetchone()[0]
|
||||
_logger.debug('Current connection id: %s', self.connection_id)
|
||||
try:
|
||||
res = self.run('select connection_id()')
|
||||
for title, cur, headers, status in res:
|
||||
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):
|
||||
self.conn.select_db(db)
|
||||
|
|
|
@ -10,4 +10,8 @@ autopep8==1.3.3
|
|||
colorama>=0.4.1
|
||||
git+https://github.com/hayd/pep8radius.git # --error-status option not released
|
||||
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
|
||||
|
|
10
setup.py
10
setup.py
|
@ -18,12 +18,15 @@ description = 'CLI for MySQL Database. With auto-completion and syntax highlight
|
|||
|
||||
install_requirements = [
|
||||
'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',
|
||||
'prompt_toolkit>=3.0.6,<4.0.0',
|
||||
'PyMySQL >= 0.9.2',
|
||||
'sqlparse>=0.3.0,<0.5.0',
|
||||
'sqlglot>=5.1.3',
|
||||
'configobj >= 5.0.5',
|
||||
'cli_helpers[styles] >= 2.2.1',
|
||||
'pyperclip >= 1.8.1',
|
||||
|
@ -101,16 +104,17 @@ setup(
|
|||
'console_scripts': ['mycli = mycli.main:cli'],
|
||||
},
|
||||
cmdclass={'lint': lint, 'test': test},
|
||||
python_requires=">=3.6",
|
||||
python_requires=">=3.7",
|
||||
classifiers=[
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: Unix',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: SQL',
|
||||
'Topic :: Database',
|
||||
'Topic :: Database :: Front-Ends',
|
||||
|
|
|
@ -6,8 +6,8 @@ import mycli.sqlexecute
|
|||
|
||||
@pytest.fixture(scope="function")
|
||||
def connection():
|
||||
create_db('_test_db')
|
||||
connection = db_connection('_test_db')
|
||||
create_db('mycli_test_db')
|
||||
connection = db_connection('mycli_test_db')
|
||||
yield connection
|
||||
|
||||
connection.close()
|
||||
|
@ -22,7 +22,7 @@ def cursor(connection):
|
|||
@pytest.fixture
|
||||
def executor(connection):
|
||||
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,
|
||||
local_infile=False, ssl=None, ssh_user=SSH_USER, ssh_host=SSH_HOST,
|
||||
ssh_port=SSH_PORT, ssh_password=None, ssh_key_filename=None
|
||||
|
|
|
@ -542,7 +542,14 @@ def test_favorite_name_suggestion(expression):
|
|||
suggestions = suggest_type(expression, expression)
|
||||
assert suggestions == [{'type': 'favoritequery'}]
|
||||
|
||||
|
||||
def test_order_by():
|
||||
text = 'select * from foo order by '
|
||||
suggestions = suggest_type(text, text)
|
||||
assert suggestions == [{'tables': [(None, 'foo', None)], 'type': 'column'}]
|
||||
|
||||
|
||||
def test_quoted_where():
|
||||
text = "'where i=';"
|
||||
suggestions = suggest_type(text, text)
|
||||
assert suggestions == [{'type': 'keyword'}]
|
||||
|
|
|
@ -25,7 +25,7 @@ os.environ['MYSQL_TEST_LOGIN_FILE'] = login_path_file
|
|||
CLI_ARGS = ['--user', USER, '--host', HOST, '--port', PORT,
|
||||
'--password', PASSWORD, '--myclirc', default_config_file,
|
||||
'--defaults-file', default_config_file,
|
||||
'_test_db']
|
||||
'mycli_test_db']
|
||||
|
||||
|
||||
@dbtest
|
||||
|
@ -283,6 +283,20 @@ def test_list_dsn():
|
|||
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():
|
||||
runner = CliRunner()
|
||||
with NamedTemporaryFile(mode="w") as ssh_config:
|
||||
|
@ -305,19 +319,25 @@ def test_dsn(monkeypatch):
|
|||
# Setup classes to mock mycli.main.MyCli
|
||||
class Formatter:
|
||||
format_name = None
|
||||
|
||||
class Logger:
|
||||
def debug(self, *args, **args_dict):
|
||||
pass
|
||||
|
||||
def warning(self, *args, **args_dict):
|
||||
pass
|
||||
|
||||
class MockMyCli:
|
||||
config = {'alias_dsn': {}}
|
||||
|
||||
def __init__(self, **args):
|
||||
self.logger = Logger()
|
||||
self.destructive_warning = False
|
||||
self.formatter = Formatter()
|
||||
|
||||
def connect(self, **args):
|
||||
MockMyCli.connect_args = args
|
||||
|
||||
def run_query(self, query, new_line=True):
|
||||
pass
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ def test_table_and_columns_query(executor):
|
|||
@dbtest
|
||||
def test_database_list(executor):
|
||||
databases = executor.databases()
|
||||
assert '_test_db' in databases
|
||||
assert 'mycli_test_db' in databases
|
||||
|
||||
|
||||
@dbtest
|
||||
|
@ -276,6 +276,7 @@ def test_multiple_results(executor):
|
|||
@pytest.mark.parametrize(
|
||||
'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-0ubuntu0.18.04.1', 'MySQL', '5.7.32', 50732),
|
||||
('10.5.8-MariaDB-1:10.5.8+maria~focal', 'MariaDB', '10.5.8', 100508),
|
||||
|
|
|
@ -41,8 +41,8 @@ dbtest = pytest.mark.skipif(
|
|||
def create_db(dbname):
|
||||
with db_connection().cursor() as cur:
|
||||
try:
|
||||
cur.execute('''DROP DATABASE IF EXISTS _test_db''')
|
||||
cur.execute('''CREATE DATABASE _test_db''')
|
||||
cur.execute('''DROP DATABASE IF EXISTS mycli_test_db''')
|
||||
cur.execute('''CREATE DATABASE mycli_test_db''')
|
||||
except:
|
||||
pass
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue