Repository: dbcli/mycli

Issue:
Allow setting SSL options (at least) as DSN URI query parameters

### Discussed in https://github.com/dbcli/mycli/discussions/1236

<div type='discussions-op-text'>

<sup>Originally posted by **ericgray** June  3, 2025</sup>
I am connecting to a MySQL server that has `require_secure_transport=ON` so I need to use `mycli --ssl` to connect, and it does work. However, I cannot determine where I can put this in the config file so I do not need it on the command line.

 </div>


Discussion:

Patch:
diff --git a/changelog.md b/changelog.md
index 9d8d4db0..47d16f8f 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,6 +1,11 @@
 Upcoming Release (TBD)
 ======================
 
+Features
+--------
+
+* Support SSL query parameters on DSNs.
+
 Internal
 --------
 
diff --git a/mycli/main.py b/mycli/main.py
index 7b8018ec..5a787988 100755
--- a/mycli/main.py
+++ b/mycli/main.py
@@ -16,7 +16,7 @@
 import itertools
 from random import choice
 from time import time
-from urllib.parse import unquote, urlparse
+from urllib.parse import parse_qs, unquote, urlparse
 
 from cli_helpers.tabular_output import TabularOutputFormatter, preprocessors
 from cli_helpers.utils import strip_ansi
@@ -1119,7 +1119,7 @@ def get_last_query(self):
 @click.option(
     "--ssl-verify-server-cert",
     is_flag=True,
-    help=('Verify server\'s "Common Name" in its cert against hostname used when connecting. This option is disabled by default.'),
+    help=("""Verify server's "Common Name" in its cert against hostname used when connecting. This option is disabled by default."""),
 )
 # as of 2016-02-15 revocation list is not supported by underling PyMySQL
 # library (--ssl-crl and --ssl-crlpath options in vanilla mysql client)
@@ -1240,20 +1240,6 @@ def cli(
     # Choose which ever one has a valid value.
     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),
-        "capath": ssl_capath,
-        "cipher": ssl_cipher,
-        "tls_version": tls_version,
-        "check_hostname": ssl_verify_server_cert,
-    }
-
-    # remove empty ssl options
-    ssl = {k: v for k, v in ssl.items() if v is not None}
-
     dsn_uri = None
 
     # Treat the database argument as a DSN alias only if it matches a configured alias
@@ -1294,6 +1280,49 @@ def cli(
         if not port:
             port = uri.port
 
+        if uri.query:
+            dsn_params = parse_qs(uri.query)
+        else:
+            dsn_params = {}
+
+        if dsn_params.get('ssl'):
+            ssl_enable = ssl_enable or (dsn_params.get('ssl')[0].lower() == 'true')
+        if dsn_params.get('ssl_ca'):
+            ssl_ca = ssl_ca or dsn_params.get('ssl_ca')[0]
+            ssl_enable = True
+        if dsn_params.get('ssl_capath'):
+            ssl_capath = ssl_capath or dsn_params.get('ssl_capath')[0]
+            ssl_enable = True
+        if dsn_params.get('ssl_cert'):
+            ssl_cert = ssl_cert or dsn_params.get('ssl_cert')[0]
+            ssl_enable = True
+        if dsn_params.get('ssl_key'):
+            ssl_key = ssl_key or dsn_params.get('ssl_key')[0]
+            ssl_enable = True
+        if dsn_params.get('ssl_cipher'):
+            ssl_cipher = ssl_cipher or dsn_params.get('ssl_cipher')[0]
+            ssl_enable = True
+        if dsn_params.get('tls_version'):
+            tls_version = tls_version or dsn_params.get('tls_version')[0]
+            ssl_enable = True
+        if dsn_params.get('ssl_verify_server_cert'):
+            ssl_verify_server_cert = ssl_verify_server_cert or (dsn_params.get('ssl_verify_server_cert')[0].lower() == 'true')
+            ssl_enable = True
+
+    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),
+        "capath": ssl_capath,
+        "cipher": ssl_cipher,
+        "tls_version": tls_version,
+        "check_hostname": ssl_verify_server_cert,
+    }
+
+    # remove empty ssl options
+    ssl = {k: v for k, v in ssl.items() if v is not None}
+
     if ssh_config_host:
         ssh_config = read_ssh_config(ssh_config_path).lookup(ssh_config_host)
         ssh_host = ssh_host if ssh_host else ssh_config.get("hostname")


Test patch:
diff --git a/test/test_main.py b/test/test_main.py
index d0c01141..bdb444fe 100644
--- a/test/test_main.py
+++ b/test/test_main.py
@@ -433,6 +433,37 @@ def run_query(self, query, new_line=True):
         and MockMyCli.connect_args["database"] == "dsn_database"
     )
 
+    # Use a DSN with query parameters
+    result = runner.invoke(mycli.main.cli, args=["mysql://dsn_user:dsn_passwd@dsn_host:6/dsn_database?ssl=True"])
+    assert result.exit_code == 0, result.output + " " + str(result.exception)
+    assert (
+        MockMyCli.connect_args["user"] == "dsn_user"
+        and MockMyCli.connect_args["passwd"] == "dsn_passwd"
+        and MockMyCli.connect_args["host"] == "dsn_host"
+        and MockMyCli.connect_args["port"] == 6
+        and MockMyCli.connect_args["database"] == "dsn_database"
+        and MockMyCli.connect_args["ssl"]["enable"] is True
+    )
+
+    # When a user uses a DSN with query parameters, and used command line
+    # arguments, use the command line arguments.
+    result = runner.invoke(
+        mycli.main.cli,
+        args=[
+            "mysql://dsn_user:dsn_passwd@dsn_host:6/dsn_database?ssl=False",
+            "--ssl",
+        ],
+    )
+    assert result.exit_code == 0, result.output + " " + str(result.exception)
+    assert (
+        MockMyCli.connect_args["user"] == "dsn_user"
+        and MockMyCli.connect_args["passwd"] == "dsn_passwd"
+        and MockMyCli.connect_args["host"] == "dsn_host"
+        and MockMyCli.connect_args["port"] == 6
+        and MockMyCli.connect_args["database"] == "dsn_database"
+        and MockMyCli.connect_args["ssl"]["enable"] is True
+    )
+
 
 def test_ssh_config(monkeypatch):
     # Setup classes to mock mycli.main.MyCli