diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce359d8..6cd8675 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,7 @@ jobs: env: PYTEST_PASSWORD: root PYTEST_HOST: 127.0.0.1 + TERM: xterm run: | uv run tox -e py${{ matrix.python-version }} diff --git a/changelog.md b/changelog.md index a418a38..5622e6d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,13 @@ +1.30.0 (2025/04/19) +=================== + +Features +-------- + +* DSN specific init-command in myclirc. Fixes (#1195) +* Add `\\g` to force the horizontal output. + + 1.29.2 (2024/12/11) =================== diff --git a/mycli/AUTHORS b/mycli/AUTHORS index b834452..7149be5 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -32,6 +32,7 @@ Contributors: * Daniel West * Daniël van Eeden * Fabrizio Gennari + * FatBoyXPC * François Pietka * Frederic Aoustin * Georgy Frolov diff --git a/mycli/main.py b/mycli/main.py index e480fea..c5963a7 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -10,6 +10,8 @@ import re import stat from collections import namedtuple +from pygments.lexer import combined + try: from pwd import getpwuid except ImportError: @@ -524,14 +526,14 @@ class MyCli(object): port = int(port) except ValueError: self.echo("Error: Invalid port number: '{0}'.".format(port), err=True, fg="red") - exit(1) + sys.exit(1) _connect() except Exception as e: # Connecting to a database could fail. self.logger.debug("Database connection failed: %r.", e) self.logger.error("traceback: %r", traceback.format_exc()) self.echo(str(e), err=True, fg="red") - exit(1) + sys.exit(1) def get_password_from_file(self, password_file): password_from_file = None @@ -674,6 +676,7 @@ class MyCli(object): return special.set_expanded_output(False) + special.set_forced_horizontal_output(False) try: text = self.handle_editor_command(text) @@ -743,6 +746,9 @@ class MyCli(object): else: max_width = None + if special.forced_horizontal(): + max_width = None + formatted = self.format_output(title, cur, headers, special.is_expanded_output(), max_width) t = time() - start @@ -1224,10 +1230,10 @@ def cli( alias_dsn = mycli.config["alias_dsn"] except KeyError: click.secho("Invalid DSNs found in the config file. " 'Please check the "[alias_dsn]" section in myclirc.', err=True, fg="red") - exit(1) + sys.exit(1) except Exception as e: click.secho(str(e), err=True, fg="red") - exit(1) + sys.exit(1) for alias, value in alias_dsn.items(): if verbose: click.secho("{} : {}".format(alias, value)) @@ -1262,9 +1268,13 @@ def cli( dsn_uri = None - # Treat the database argument as a DSN alias if we're missing - # other connection information. - if mycli.config["alias_dsn"] and database and "://" not in database and not any([user, password, host, port, login_path]): + # Treat the database argument as a DSN alias only if it matches a configured alias + if ( + database + and "://" not in database + and not any([user, password, host, port, login_path]) + and database in mycli.config.get("alias_dsn", {}) + ): dsn, database = database, "" if database and "://" in database: @@ -1279,7 +1289,7 @@ def cli( err=True, fg="red", ) - exit(1) + sys.exit(1) else: mycli.dsn_alias = dsn @@ -1306,6 +1316,29 @@ def cli( ssh_key_filename = ssh_key_filename if ssh_key_filename else ssh_config.get("identityfile", [None])[0] ssh_key_filename = ssh_key_filename and os.path.expanduser(ssh_key_filename) + # Merge init-commands: global, DSN-specific, then CLI + init_cmds = [] + # 1) Global init-commands + global_section = mycli.config.get("init-commands", {}) + for _, val in global_section.items(): + if isinstance(val, (list, tuple)): + init_cmds.extend(val) + elif val: + init_cmds.append(val) + # 2) DSN-specific init-commands + if dsn: + alias_section = mycli.config.get("alias_dsn.init-commands", {}) + if dsn in alias_section: + val = alias_section.get(dsn) + if isinstance(val, (list, tuple)): + init_cmds.extend(val) + elif val: + init_cmds.append(val) + # 3) CLI-provided init_command + if init_command: + init_cmds.append(init_command) + + combined_init_cmd = "; ".join(cmd.strip() for cmd in init_cmds if cmd) mycli.connect( database=database, @@ -1321,11 +1354,14 @@ def cli( ssh_port=ssh_port, ssh_password=ssh_password, ssh_key_filename=ssh_key_filename, - init_command=init_command, + init_command=combined_init_cmd, charset=charset, password_file=password_file, ) + if combined_init_cmd: + click.echo("Executing init-command: %s" % combined_init_cmd, err=True) + mycli.logger.debug("Launch Params: \n" "\tdatabase: %r" "\tuser: %r" "\thost: %r" "\tport: %r", database, user, host, port) # --execute argument @@ -1342,10 +1378,10 @@ def cli( mycli.formatter.format_name = "tsv" mycli.run_query(execute) - exit(0) + sys.exit(0) except Exception as e: click.secho(str(e), err=True, fg="red") - exit(1) + sys.exit(1) if sys.stdin.isatty(): mycli.run_cli() @@ -1357,7 +1393,7 @@ def cli( click.secho("Failed! Ran out of memory.", err=True, fg="red") click.secho("You might want to try the official mysql client.", err=True, fg="red") click.secho("Sorry... :(", err=True, fg="red") - exit(1) + sys.exit(1) if mycli.destructive_warning and is_destructive(stdin_text): try: @@ -1366,7 +1402,7 @@ def cli( except (IOError, OSError): mycli.logger.warning("Unable to open TTY as stdin.") if not warn_confirmed: - exit(0) + sys.exit(0) try: new_line = True @@ -1377,10 +1413,10 @@ def cli( mycli.formatter.format_name = "tsv" mycli.run_query(stdin_text, new_line=new_line) - exit(0) + sys.exit(0) except Exception as e: click.secho(str(e), err=True, fg="red") - exit(1) + sys.exit(1) def need_completion_refresh(queries): diff --git a/mycli/myclirc b/mycli/myclirc index cd58dfe..096cfe5 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -151,9 +151,22 @@ output.null = "#808080" # sql.whitespace = '' # Favorite queries. +# You can add your favorite queries here. They will be available in the +# REPL when you type `\f` or `\f `. [favorite_queries] +# example = "SELECT * FROM example_table WHERE id = 1" + +# Initial commands to execute when connecting to any database. +[init-commands] +# read_only = "SET SESSION TRANSACTION READ ONLY" + # Use the -d option to reference a DSN. # Special characters in passwords and other strings can be escaped with URL encoding. [alias_dsn] # example_dsn = mysql://[user[:password]@][host][:port][/dbname] + +# Initial commands to execute when connecting to a DSN alias. +[alias_dsn.init-commands] +# Define one or more SQL statements per alias (semicolon-separated). +# example_dsn = "SET sql_select_limit=1000; SET time_zone='+00:00'" diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py index 87b5366..e3950c3 100644 --- a/mycli/packages/special/iocommands.py +++ b/mycli/packages/special/iocommands.py @@ -20,6 +20,7 @@ from mycli.packages.prompt_utils import confirm_destructive_query TIMING_ENABLED = False use_expanded_output = False +force_horizontal_output = False PAGER_ENABLED = True tee_file = None once_file = None @@ -97,6 +98,14 @@ def set_expanded_output(val): def is_expanded_output(): return use_expanded_output +@export +def set_forced_horizontal_output(val): + global force_horizontal_output + force_horizontal_output = val + +@export +def forced_horizontal(): + return force_horizontal_output _logger = logging.getLogger(__name__) diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index d5b6db6..cabde71 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -233,7 +233,7 @@ class SQLExecute(object): ssl=ssl_context, program_name="mycli", defer_connect=defer_connect, - init_command=init_command, + init_command=init_command or None, ) if ssh_host: @@ -298,6 +298,12 @@ class SQLExecute(object): if sql.endswith("\\G"): special.set_expanded_output(True) sql = sql[:-2].strip() + # \g is treated specially since we might want collapsed output when + # auto vertical output is enabled + elif sql.endswith('\\g'): + special.set_expanded_output(False) + special.set_forced_horizontal_output(True) + sql = sql[:-2].strip() cur = self.conn.cursor() try: # Special command diff --git a/pyproject.toml b/pyproject.toml index 107e85b..5712dec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [{ name = "Mycli Core Team", email = "mycli-dev@googlegroups.com" }] urls = { homepage = "http://mycli.net" } dependencies = [ - "click >= 7.0", + "click >= 7.0,<8.1.8", "cryptography >= 1.0.0", "Pygments>=1.6", "prompt_toolkit>=3.0.6,<4.0.0", diff --git a/test/myclirc b/test/myclirc index 58f7279..fef49f2 100644 --- a/test/myclirc +++ b/test/myclirc @@ -151,11 +151,25 @@ output.null = "#808080" # sql.whitespace = '' # Favorite queries. +# You can add your favorite queries here. They will be available in the +# REPL when you type `\f` or `\f `. [favorite_queries] check = 'select "✔"' foo_args = 'SELECT $1, "$2", "$3"' +# example = "SELECT * FROM example_table WHERE id = 1" + +# Initial commands to execute when connecting to any database. +[init-commands] +# read_only = "SET SESSION TRANSACTION READ ONLY" +global_limit = "set sql_select_limit=9999" + # Use the -d option to reference a DSN. # Special characters in passwords and other strings can be escaped with URL encoding. [alias_dsn] # example_dsn = mysql://[user[:password]@][host][:port][/dbname] + +# Initial commands to execute when connecting to a DSN alias. +[alias_dsn.init-commands] +# Define one or more SQL statements per alias (semicolon-separated). +# example_dsn = "SET sql_select_limit=1000; SET time_zone='+00:00'" diff --git a/test/test_main.py b/test/test_main.py index b0f8d4c..3a757bc 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -553,3 +553,14 @@ def test_init_command_multiple_arg(executor): assert result.exit_code == 0 assert expected_sql_select_limit in result.output assert expected_max_join_size in result.output + +@dbtest +def test_global_init_commands(executor): + """Tests that global init-commands from config are executed by default.""" + # The global init-commands section in test/myclirc sets sql_select_limit=9999 + sql = 'show variables like "sql_select_limit";' + runner = CliRunner() + result = runner.invoke(cli, args=CLI_ARGS, input=sql) + expected = "sql_select_limit\t9999\n" + assert result.exit_code == 0 + assert expected in result.output diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py index 17e082b..f71deea 100644 --- a/test/test_sqlexecute.py +++ b/test/test_sqlexecute.py @@ -173,6 +173,13 @@ def test_favorite_query_expanded_output(executor): assert_result_equal(results, status="test-ae: Deleted") +@dbtest +def test_collapsed_output_special_command(executor): + set_expanded_output(True) + run(executor, "select 1\\g") + assert is_expanded_output() is False + + @dbtest def test_special_command(executor): results = run(executor, "\\?")