diff options
-rw-r--r-- | .github/workflows/ci.yml | 16 | ||||
-rw-r--r-- | CONTRIBUTING.md | 10 | ||||
-rw-r--r-- | README.md | 25 | ||||
-rw-r--r-- | changelog.md | 35 | ||||
-rw-r--r-- | doc/key_bindings.rst | 65 | ||||
-rw-r--r-- | mycli/AUTHORS | 6 | ||||
-rw-r--r-- | mycli/__init__.py | 2 | ||||
-rw-r--r-- | mycli/clitoolbar.py | 5 | ||||
-rw-r--r-- | mycli/completion_refresher.py | 2 | ||||
-rw-r--r-- | mycli/key_bindings.py | 44 | ||||
-rwxr-xr-x | mycli/main.py | 80 | ||||
-rw-r--r-- | mycli/myclirc | 7 | ||||
-rw-r--r-- | mycli/packages/completion_engine.py | 2 | ||||
-rw-r--r-- | mycli/packages/parseutils.py | 2 | ||||
-rw-r--r-- | mycli/packages/special/dbcommands.py | 5 | ||||
-rw-r--r-- | mycli/sqlexecute.py | 16 | ||||
-rw-r--r-- | requirements-dev.txt | 6 | ||||
-rwxr-xr-x | setup.py | 10 | ||||
-rw-r--r-- | test/conftest.py | 6 | ||||
-rw-r--r-- | test/test_completion_engine.py | 7 | ||||
-rw-r--r-- | test/test_main.py | 22 | ||||
-rw-r--r-- | test/test_sqlexecute.py | 3 | ||||
-rw-r--r-- | test/utils.py | 4 |
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 @@ -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 @@ -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 |