summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml16
-rw-r--r--CONTRIBUTING.md10
-rw-r--r--README.md25
-rw-r--r--changelog.md35
-rw-r--r--doc/key_bindings.rst65
-rw-r--r--mycli/AUTHORS6
-rw-r--r--mycli/__init__.py2
-rw-r--r--mycli/clitoolbar.py5
-rw-r--r--mycli/completion_refresher.py2
-rw-r--r--mycli/key_bindings.py44
-rwxr-xr-xmycli/main.py80
-rw-r--r--mycli/myclirc7
-rw-r--r--mycli/packages/completion_engine.py2
-rw-r--r--mycli/packages/parseutils.py2
-rw-r--r--mycli/packages/special/dbcommands.py5
-rw-r--r--mycli/sqlexecute.py16
-rw-r--r--requirements-dev.txt6
-rwxr-xr-xsetup.py10
-rw-r--r--test/conftest.py6
-rw-r--r--test/test_completion_engine.py7
-rw-r--r--test/test_main.py22
-rw-r--r--test/test_sqlexecute.py3
-rw-r--r--test/utils.py4
23 files changed, 328 insertions, 52 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b678f57..752ddb5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3075600..cac4f04 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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
diff --git a/README.md b/README.md
index 1fefeec..9e177b7 100644
--- a/README.md
+++ b/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
diff --git a/changelog.md b/changelog.md
index 01040ae..3dbdc1f 100644
--- a/changelog.md
+++ b/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
diff --git a/doc/key_bindings.rst b/doc/key_bindings.rst
new file mode 100644
index 0000000..0534870
--- /dev/null
+++ b/doc/key_bindings.rst
@@ -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.
diff --git a/mycli/AUTHORS b/mycli/AUTHORS
index d1f3a28..a805465 100644
--- a/mycli/AUTHORS
+++ b/mycli/AUTHORS
@@ -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:
-----------
diff --git a/mycli/__init__.py b/mycli/__init__.py
index 8de33c0..1512b41 100644
--- a/mycli/__init__.py
+++ b/mycli/__init__.py
@@ -1 +1 @@
-__version__ = '1.25.0'
+__version__ = '1.26.1'
diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py
index eec2978..24d1108 100644
--- a/mycli/clitoolbar.py
+++ b/mycli/clitoolbar.py
@@ -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'))
diff --git a/mycli/completion_refresher.py b/mycli/completion_refresher.py
index 124068a..8eb3de9 100644
--- a/mycli/completion_refresher.py
+++ b/mycli/completion_refresher.py
@@ -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,
diff --git a/mycli/key_bindings.py b/mycli/key_bindings.py
index 4a24c82..03e4ace 100644
--- a/mycli/key_bindings.py
+++ b/mycli/key_bindings.py
@@ -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.
diff --git a/mycli/main.py b/mycli/main.py
index 08f0755..208572d 100755
--- a/mycli/main.py
+++ b/mycli/main.py
@@ -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),
diff --git a/mycli/myclirc b/mycli/myclirc
index 2418342..cd58dfe 100644
--- a/mycli/myclirc
+++ b/mycli/myclirc
@@ -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'
diff --git a/mycli/packages/completion_engine.py b/mycli/packages/completion_engine.py
index c7db06c..2735f5b 100644
--- a/mycli/packages/completion_engine.py
+++ b/mycli/packages/completion_engine.py
@@ -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()
diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py
index d47f59a..3090530 100644
--- a/mycli/packages/parseutils.py
+++ b/mycli/packages/parseutils.py
@@ -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
diff --git a/mycli/packages/special/dbcommands.py b/mycli/packages/special/dbcommands.py
index 45d7069..5c29c55 100644
--- a/mycli/packages/special/dbcommands.py
+++ b/mycli/packages/special/dbcommands.py
@@ -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'])
diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py
index 9461438..c019707 100644
--- a/mycli/sqlexecute.py
+++ b/mycli/sqlexecute.py
@@ -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)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 107106d..955a9f5 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -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
diff --git a/setup.py b/setup.py
index a44289a..2f69672 100755
--- a/setup.py
+++ b/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',
diff --git a/test/conftest.py b/test/conftest.py
index d7d10ce..1325596 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -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
diff --git a/test/test_completion_engine.py b/test/test_completion_engine.py
index 8b06ed3..318b632 100644
--- a/test/test_completion_engine.py
+++ b/test/test_completion_engine.py
@@ -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'}]
diff --git a/test/test_main.py b/test/test_main.py
index 7731603..64cba0a 100644
--- a/test/test_main.py
+++ b/test/test_main.py
@@ -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
diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py
index 0f38a97..38ca5ef 100644
--- a/test/test_sqlexecute.py
+++ b/test/test_sqlexecute.py
@@ -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),
diff --git a/test/utils.py b/test/utils.py
index 66b4194..ab12248 100644
--- a/test/utils.py
+++ b/test/utils.py
@@ -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