From 6adba1c4aaac49f370f6ff3b9773decde302f367 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 18 Apr 2025 19:34:21 -0700 Subject: [PATCH 1/9] wip --- mycli/main.py | 27 ++++++++++++++++++++++++++- mycli/myclirc | 14 ++++++++++++++ test/myclirc | 7 +++++++ test/test_main.py | 11 +++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/mycli/main.py b/mycli/main.py index be15e343..b241a037 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1306,7 +1306,32 @@ 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', {}) + if isinstance(global_section, dict): + 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 isinstance(alias_section, dict) and 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) + # Compose into single semicolon-separated string + if init_cmds: + init_command = '; '.join(cmd.strip() for cmd in init_cmds if cmd) + mycli.connect( database=database, user=user, diff --git a/mycli/myclirc b/mycli/myclirc index cd58dfe2..8c1d90d9 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -151,9 +151,23 @@ 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] +# "SET SESSION TRANSACTION READ ONLY" +# "SELECT version()" + # 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/myclirc b/test/myclirc index 58f72799..5992c612 100644 --- a/test/myclirc +++ b/test/myclirc @@ -159,3 +159,10 @@ foo_args = 'SELECT $1, "$2", "$3"' # 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] +[init-commands] +global_limit = set sql_select_limit=9999 +# 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 b0f8d4c0..3a757bcc 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 From 3a785e4563e409e2140cf5d2fab4b203cd4df9e3 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 18 Apr 2025 19:44:41 -0700 Subject: [PATCH 2/9] Fix the unknown dsn error when invoked with a dbname. --- mycli/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index b241a037..ee62d817 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1262,9 +1262,9 @@ 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: From b44dceeb9220ce0a3f13a320f98e8ec9bb4c4d36 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 18 Apr 2025 20:50:01 -0700 Subject: [PATCH 3/9] Echo the init-commands. --- mycli/main.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index ee62d817..710de551 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1263,8 +1263,12 @@ def cli( dsn_uri = None # 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", {}): + 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: @@ -1309,17 +1313,16 @@ def cli( # Merge init-commands: global, DSN-specific, then CLI init_cmds = [] # 1) Global init-commands - global_section = mycli.config.get('init-commands', {}) - if isinstance(global_section, dict): - for _, val in global_section.items(): - if isinstance(val, (list, tuple)): - init_cmds.extend(val) - elif val: - init_cmds.append(val) + 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 isinstance(alias_section, dict) and dsn in alias_section: + 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) @@ -1328,10 +1331,7 @@ def cli( # 3) CLI-provided init_command if init_command: init_cmds.append(init_command) - # Compose into single semicolon-separated string - if init_cmds: - init_command = '; '.join(cmd.strip() for cmd in init_cmds if cmd) - + mycli.connect( database=database, user=user, @@ -1351,6 +1351,14 @@ def cli( password_file=password_file, ) + if init_cmds: + init_command = "; ".join(cmd.strip() for cmd in init_cmds if cmd) + # Provide user feedback on which init commands are executed + mycli.echo("Running init commands:", err=True) + for cmd in init_cmds: + # Display each SQL init command + mycli.echo(cmd.strip(), err=True) + mycli.logger.debug("Launch Params: \n" "\tdatabase: %r" "\tuser: %r" "\thost: %r" "\tport: %r", database, user, host, port) # --execute argument From 6f487ac66bd5ab1c7f07e176b92f53c404f1408e Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 18 Apr 2025 21:37:18 -0700 Subject: [PATCH 4/9] Print the init_command at startup. --- mycli/main.py | 12 +++--------- mycli/myclirc | 3 +-- mycli/sqlexecute.py | 3 +++ test/myclirc | 11 +++++++++-- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 710de551..3ca0b1d1 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1332,6 +1332,8 @@ def cli( 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, user=user, @@ -1346,19 +1348,11 @@ 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 init_cmds: - init_command = "; ".join(cmd.strip() for cmd in init_cmds if cmd) - # Provide user feedback on which init commands are executed - mycli.echo("Running init commands:", err=True) - for cmd in init_cmds: - # Display each SQL init command - mycli.echo(cmd.strip(), err=True) - mycli.logger.debug("Launch Params: \n" "\tdatabase: %r" "\tuser: %r" "\thost: %r" "\tport: %r", database, user, host, port) # --execute argument diff --git a/mycli/myclirc b/mycli/myclirc index 8c1d90d9..096cfe57 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -158,8 +158,7 @@ output.null = "#808080" # Initial commands to execute when connecting to any database. [init-commands] -# "SET SESSION TRANSACTION READ ONLY" -# "SELECT version()" +# read_only = "SET SESSION TRANSACTION READ ONLY" # Use the -d option to reference a DSN. diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index d5b6db6f..a591cbf0 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -236,6 +236,9 @@ def connect( init_command=init_command, ) + if init_command: + print("Running init commands:\n", init_command) + if ssh_host: ##### paramiko.Channel is a bad socket implementation overall if you want SSL through an SSH tunnel ##### diff --git a/test/myclirc b/test/myclirc index 5992c612..bd590158 100644 --- a/test/myclirc +++ b/test/myclirc @@ -151,9 +151,18 @@ 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] +global_limit = set sql_select_limit=9999 +# 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. @@ -162,7 +171,5 @@ foo_args = 'SELECT $1, "$2", "$3"' # Initial commands to execute when connecting to a DSN alias. [alias_dsn.init-commands] -[init-commands] -global_limit = set sql_select_limit=9999 # Define one or more SQL statements per alias (semicolon-separated). # example_dsn = "SET sql_select_limit=1000; SET time_zone='+00:00'" From 151cf9a05706242c12033a8397a83883eb3b07e6 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 18 Apr 2025 21:38:34 -0700 Subject: [PATCH 5/9] Update the myclirc test file. --- test/myclirc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/myclirc b/test/myclirc index bd590158..fef49f2d 100644 --- a/test/myclirc +++ b/test/myclirc @@ -160,8 +160,8 @@ foo_args = 'SELECT $1, "$2", "$3"' # Initial commands to execute when connecting to any database. [init-commands] -global_limit = set sql_select_limit=9999 # read_only = "SET SESSION TRANSACTION READ ONLY" +global_limit = "set sql_select_limit=9999" # Use the -d option to reference a DSN. From 9e2ee718aae6ec960755f1ff590d6d0ddb921f91 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Fri, 18 Apr 2025 22:00:00 -0700 Subject: [PATCH 6/9] Update changelog. --- changelog.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/changelog.md b/changelog.md index a418a380..816884c7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,13 @@ +Upcoming Release (TBD) +====================== + +Features +-------- + +* DSN specific init-command in myclirc. Fixes (#1195) + + + 1.29.2 (2024/12/11) =================== From f6788e7b3e557e1b06e77ba302c25ffa59cfe774 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 19 Apr 2025 08:49:02 -0700 Subject: [PATCH 7/9] Pass None if init-commands are empty. --- mycli/sqlexecute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index a591cbf0..05d0fadf 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -233,7 +233,7 @@ def connect( ssl=ssl_context, program_name="mycli", defer_connect=defer_connect, - init_command=init_command, + init_command=init_cmd or None, ) if init_command: From f76009c5bc52894220622d9283123c8281fcd3db Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 19 Apr 2025 08:56:46 -0700 Subject: [PATCH 8/9] Print the init command to stderr. --- mycli/main.py | 5 +++++ mycli/sqlexecute.py | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 3ca0b1d1..2342e663 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -10,6 +10,8 @@ import stat from collections import namedtuple +from pygments.lexer import combined + try: from pwd import getpwuid except ImportError: @@ -1353,6 +1355,9 @@ def cli( 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 diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py index 05d0fadf..7327be3b 100644 --- a/mycli/sqlexecute.py +++ b/mycli/sqlexecute.py @@ -233,12 +233,9 @@ def connect( ssl=ssl_context, program_name="mycli", defer_connect=defer_connect, - init_command=init_cmd or None, + init_command=init_command or None, ) - if init_command: - print("Running init commands:\n", init_command) - if ssh_host: ##### paramiko.Channel is a bad socket implementation overall if you want SSL through an SSH tunnel ##### From bb18b0c2f2ed7375efe31d379e616a11c82b1299 Mon Sep 17 00:00:00 2001 From: Amjith Ramanujam Date: Sat, 19 Apr 2025 12:42:36 -0700 Subject: [PATCH 9/9] Downgrade click to previous version. Latest patch version of click 8.1.8 is breaking behave tests. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 107e85b9..5712decd 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",