1
0
Fork 0

Merging upstream version 1.30.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-04-21 09:44:12 +02:00
parent 1b2f180fc6
commit 1fa9e0bfa5
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
11 changed files with 125 additions and 17 deletions

View file

@ -42,6 +42,7 @@ jobs:
env: env:
PYTEST_PASSWORD: root PYTEST_PASSWORD: root
PYTEST_HOST: 127.0.0.1 PYTEST_HOST: 127.0.0.1
TERM: xterm
run: | run: |
uv run tox -e py${{ matrix.python-version }} uv run tox -e py${{ matrix.python-version }}

View file

@ -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) 1.29.2 (2024/12/11)
=================== ===================

View file

@ -32,6 +32,7 @@ Contributors:
* Daniel West * Daniel West
* Daniël van Eeden * Daniël van Eeden
* Fabrizio Gennari * Fabrizio Gennari
* FatBoyXPC
* François Pietka * François Pietka
* Frederic Aoustin * Frederic Aoustin
* Georgy Frolov * Georgy Frolov

View file

@ -10,6 +10,8 @@ import re
import stat import stat
from collections import namedtuple from collections import namedtuple
from pygments.lexer import combined
try: try:
from pwd import getpwuid from pwd import getpwuid
except ImportError: except ImportError:
@ -524,14 +526,14 @@ class MyCli(object):
port = int(port) port = int(port)
except ValueError: except ValueError:
self.echo("Error: Invalid port number: '{0}'.".format(port), err=True, fg="red") self.echo("Error: Invalid port number: '{0}'.".format(port), err=True, fg="red")
exit(1) sys.exit(1)
_connect() _connect()
except Exception as e: # Connecting to a database could fail. except Exception as e: # Connecting to a database could fail.
self.logger.debug("Database connection failed: %r.", e) self.logger.debug("Database connection failed: %r.", e)
self.logger.error("traceback: %r", traceback.format_exc()) self.logger.error("traceback: %r", traceback.format_exc())
self.echo(str(e), err=True, fg="red") self.echo(str(e), err=True, fg="red")
exit(1) sys.exit(1)
def get_password_from_file(self, password_file): def get_password_from_file(self, password_file):
password_from_file = None password_from_file = None
@ -674,6 +676,7 @@ class MyCli(object):
return return
special.set_expanded_output(False) special.set_expanded_output(False)
special.set_forced_horizontal_output(False)
try: try:
text = self.handle_editor_command(text) text = self.handle_editor_command(text)
@ -743,6 +746,9 @@ class MyCli(object):
else: else:
max_width = None max_width = None
if special.forced_horizontal():
max_width = None
formatted = self.format_output(title, cur, headers, special.is_expanded_output(), max_width) formatted = self.format_output(title, cur, headers, special.is_expanded_output(), max_width)
t = time() - start t = time() - start
@ -1224,10 +1230,10 @@ def cli(
alias_dsn = mycli.config["alias_dsn"] alias_dsn = mycli.config["alias_dsn"]
except KeyError: except KeyError:
click.secho("Invalid DSNs found in the config file. " 'Please check the "[alias_dsn]" section in myclirc.', err=True, fg="red") 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: except Exception as e:
click.secho(str(e), err=True, fg="red") click.secho(str(e), err=True, fg="red")
exit(1) sys.exit(1)
for alias, value in alias_dsn.items(): for alias, value in alias_dsn.items():
if verbose: if verbose:
click.secho("{} : {}".format(alias, value)) click.secho("{} : {}".format(alias, value))
@ -1262,9 +1268,13 @@ def cli(
dsn_uri = None dsn_uri = None
# Treat the database argument as a DSN alias if we're missing # Treat the database argument as a DSN alias only if it matches a configured alias
# other connection information. if (
if mycli.config["alias_dsn"] and database and "://" not in database and not any([user, password, host, port, login_path]): 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, "" dsn, database = database, ""
if database and "://" in database: if database and "://" in database:
@ -1279,7 +1289,7 @@ def cli(
err=True, err=True,
fg="red", fg="red",
) )
exit(1) sys.exit(1)
else: else:
mycli.dsn_alias = dsn 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 if ssh_key_filename else ssh_config.get("identityfile", [None])[0]
ssh_key_filename = ssh_key_filename and os.path.expanduser(ssh_key_filename) 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( mycli.connect(
database=database, database=database,
@ -1321,11 +1354,14 @@ def cli(
ssh_port=ssh_port, ssh_port=ssh_port,
ssh_password=ssh_password, ssh_password=ssh_password,
ssh_key_filename=ssh_key_filename, ssh_key_filename=ssh_key_filename,
init_command=init_command, init_command=combined_init_cmd,
charset=charset, charset=charset,
password_file=password_file, 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) mycli.logger.debug("Launch Params: \n" "\tdatabase: %r" "\tuser: %r" "\thost: %r" "\tport: %r", database, user, host, port)
# --execute argument # --execute argument
@ -1342,10 +1378,10 @@ def cli(
mycli.formatter.format_name = "tsv" mycli.formatter.format_name = "tsv"
mycli.run_query(execute) mycli.run_query(execute)
exit(0) sys.exit(0)
except Exception as e: except Exception as e:
click.secho(str(e), err=True, fg="red") click.secho(str(e), err=True, fg="red")
exit(1) sys.exit(1)
if sys.stdin.isatty(): if sys.stdin.isatty():
mycli.run_cli() mycli.run_cli()
@ -1357,7 +1393,7 @@ def cli(
click.secho("Failed! Ran out of memory.", err=True, fg="red") 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("You might want to try the official mysql client.", err=True, fg="red")
click.secho("Sorry... :(", 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): if mycli.destructive_warning and is_destructive(stdin_text):
try: try:
@ -1366,7 +1402,7 @@ def cli(
except (IOError, OSError): except (IOError, OSError):
mycli.logger.warning("Unable to open TTY as stdin.") mycli.logger.warning("Unable to open TTY as stdin.")
if not warn_confirmed: if not warn_confirmed:
exit(0) sys.exit(0)
try: try:
new_line = True new_line = True
@ -1377,10 +1413,10 @@ def cli(
mycli.formatter.format_name = "tsv" mycli.formatter.format_name = "tsv"
mycli.run_query(stdin_text, new_line=new_line) mycli.run_query(stdin_text, new_line=new_line)
exit(0) sys.exit(0)
except Exception as e: except Exception as e:
click.secho(str(e), err=True, fg="red") click.secho(str(e), err=True, fg="red")
exit(1) sys.exit(1)
def need_completion_refresh(queries): def need_completion_refresh(queries):

View file

@ -151,9 +151,22 @@ output.null = "#808080"
# sql.whitespace = '' # sql.whitespace = ''
# Favorite queries. # Favorite queries.
# You can add your favorite queries here. They will be available in the
# REPL when you type `\f` or `\f <query_name>`.
[favorite_queries] [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. # Use the -d option to reference a DSN.
# Special characters in passwords and other strings can be escaped with URL encoding. # Special characters in passwords and other strings can be escaped with URL encoding.
[alias_dsn] [alias_dsn]
# example_dsn = mysql://[user[:password]@][host][:port][/dbname] # 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'"

View file

@ -20,6 +20,7 @@ from mycli.packages.prompt_utils import confirm_destructive_query
TIMING_ENABLED = False TIMING_ENABLED = False
use_expanded_output = False use_expanded_output = False
force_horizontal_output = False
PAGER_ENABLED = True PAGER_ENABLED = True
tee_file = None tee_file = None
once_file = None once_file = None
@ -97,6 +98,14 @@ def set_expanded_output(val):
def is_expanded_output(): def is_expanded_output():
return use_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__) _logger = logging.getLogger(__name__)

View file

@ -233,7 +233,7 @@ class SQLExecute(object):
ssl=ssl_context, ssl=ssl_context,
program_name="mycli", program_name="mycli",
defer_connect=defer_connect, defer_connect=defer_connect,
init_command=init_command, init_command=init_command or None,
) )
if ssh_host: if ssh_host:
@ -298,6 +298,12 @@ class SQLExecute(object):
if sql.endswith("\\G"): if sql.endswith("\\G"):
special.set_expanded_output(True) special.set_expanded_output(True)
sql = sql[:-2].strip() 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() cur = self.conn.cursor()
try: # Special command try: # Special command

View file

@ -9,7 +9,7 @@ authors = [{ name = "Mycli Core Team", email = "mycli-dev@googlegroups.com" }]
urls = { homepage = "http://mycli.net" } urls = { homepage = "http://mycli.net" }
dependencies = [ dependencies = [
"click >= 7.0", "click >= 7.0,<8.1.8",
"cryptography >= 1.0.0", "cryptography >= 1.0.0",
"Pygments>=1.6", "Pygments>=1.6",
"prompt_toolkit>=3.0.6,<4.0.0", "prompt_toolkit>=3.0.6,<4.0.0",

View file

@ -151,11 +151,25 @@ output.null = "#808080"
# sql.whitespace = '' # sql.whitespace = ''
# Favorite queries. # Favorite queries.
# You can add your favorite queries here. They will be available in the
# REPL when you type `\f` or `\f <query_name>`.
[favorite_queries] [favorite_queries]
check = 'select "✔"' check = 'select "✔"'
foo_args = 'SELECT $1, "$2", "$3"' 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. # Use the -d option to reference a DSN.
# Special characters in passwords and other strings can be escaped with URL encoding. # Special characters in passwords and other strings can be escaped with URL encoding.
[alias_dsn] [alias_dsn]
# example_dsn = mysql://[user[:password]@][host][:port][/dbname] # 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'"

View file

@ -553,3 +553,14 @@ def test_init_command_multiple_arg(executor):
assert result.exit_code == 0 assert result.exit_code == 0
assert expected_sql_select_limit in result.output assert expected_sql_select_limit in result.output
assert expected_max_join_size 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

View file

@ -173,6 +173,13 @@ def test_favorite_query_expanded_output(executor):
assert_result_equal(results, status="test-ae: Deleted") 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 @dbtest
def test_special_command(executor): def test_special_command(executor):
results = run(executor, "\\?") results = run(executor, "\\?")