diff --git a/changelog.md b/changelog.md index 21c296a8..0ddfba26 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ Features --------- * Add `--ssh-options` CLI argument to pass extra options with `--ssh-jump`. * Make `ssh_jump` a DSN query parameter. +* Warn on unknown DSN query parameters. Bug Fixes diff --git a/mycli/cli_runner.py b/mycli/cli_runner.py index f03b71c9..84565124 100644 --- a/mycli/cli_runner.py +++ b/mycli/cli_runner.py @@ -21,6 +21,21 @@ ClientFactory = Callable[..., Any] ENV_VAR_PATTERN = re.compile(r'^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$') +KNOWN_DSN_QUERY_PARAMS = { + 'character_set', + 'keepalive_ticks', + 'socket', + 'ssh_jump', + 'ssl', + 'ssl_ca', + 'ssl_capath', + 'ssl_cert', + 'ssl_cipher', + 'ssl_key', + 'ssl_mode', + 'ssl_verify_server_cert', + 'tls_version', +} class DsnAliasEnvVarError(ValueError): @@ -79,7 +94,10 @@ def expand_dsn_alias_env_vars( except ValueError as exc: raise DsnAliasEnvVarError(f'Port in DSN alias {alias_name} must be an integer.') from exc - params = {key: [expand_dsn_alias_env_var(value, alias_name) or '' for value in values] for key, values in parse_qs(uri.query).items()} + params = { + key: [expand_dsn_alias_env_var(value, alias_name) or '' for value in values] + for key, values in parse_qs(uri.query, keep_blank_values=True).items() + } return ( expand_dsn_alias_env_var(unquote(username) if username is not None else None, alias_name), @@ -200,7 +218,7 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non dsn_host = uri.hostname dsn_port = uri.port dsn_database = uri.path[1:] - dsn_params = parse_qs(uri.query) if uri.query else {} + dsn_params = parse_qs(uri.query, keep_blank_values=True) if uri.query else {} if not database: database = dsn_database @@ -214,6 +232,13 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non if not cli_args.port: cli_args.port = dsn_port + if unknown_dsn_params := sorted(set(dsn_params) - KNOWN_DSN_QUERY_PARAMS): + click.secho( + f'Warning: Ignored unknown DSN URI query parameters: {", ".join(unknown_dsn_params)}.', + err=True, + fg='yellow', + ) + if params := dsn_params.get('ssl'): click.secho( 'Warning: The "ssl" DSN URI parameter is deprecated and will be removed in a future release. ' diff --git a/test/pytests/test_cli_runner.py b/test/pytests/test_cli_runner.py index efba80a2..f1879070 100644 --- a/test/pytests/test_cli_runner.py +++ b/test/pytests/test_cli_runner.py @@ -269,6 +269,70 @@ def test_run_from_cli_args_does_not_expand_unbraced_dsn_alias_env_vars( assert client.connect_calls[-1]['user'] == '$MYCLI_TEST_DSN_USER' +def test_run_from_cli_args_warns_about_unknown_dsn_query_parameter( + monkeypatch: pytest.MonkeyPatch, +) -> None: + cli_args = make_cli_args() + cli_args.dsn = 'mysql://user@host/db?unknown=value' + client = DummyMyCli() + secho_calls: list[tuple[str, dict[str, Any]]] = [] + monkeypatch.setattr(cli_runner.click, 'secho', lambda text, **kwargs: secho_calls.append((text, kwargs))) + + run_with_client(monkeypatch, cli_args, client) + + assert client.connect_calls[-1]['database'] == 'db' + assert secho_calls == [ + ( + 'Warning: Ignored unknown DSN URI query parameters: unknown.', + {'err': True, 'fg': 'yellow'}, + ) + ] + + +def test_run_from_cli_args_warns_once_about_sorted_unknown_dsn_query_parameters( + monkeypatch: pytest.MonkeyPatch, +) -> None: + cli_args = make_cli_args() + cli_args.dsn = 'mysql://user@host/db?zzz=1&aaa&character_set=utf8&aaa=2' + client = DummyMyCli() + secho_calls: list[tuple[str, dict[str, Any]]] = [] + monkeypatch.setattr(cli_runner.click, 'secho', lambda text, **kwargs: secho_calls.append((text, kwargs))) + + run_with_client(monkeypatch, cli_args, client) + + assert client.connect_calls[-1]['character_set'] == 'utf8' + assert secho_calls == [ + ( + 'Warning: Ignored unknown DSN URI query parameters: aaa, zzz.', + {'err': True, 'fg': 'yellow'}, + ) + ] + + +def test_run_from_cli_args_warns_about_unknown_alias_dsn_query_parameter_with_env_expansion( + monkeypatch: pytest.MonkeyPatch, +) -> None: + cli_args = make_cli_args() + cli_args.dsn = 'prod' + monkeypatch.setenv('MYCLI_TEST_DSN_CHARSET', 'utf8') + config = default_config() + config['main'] = {**config['main'], 'expand_dsn_alias_env_vars': 'true'} + config['alias_dsn'] = {'prod': 'mysql://user@host/db?unknown=value&character_set=${MYCLI_TEST_DSN_CHARSET}'} + client = DummyMyCli(config=config) + secho_calls: list[tuple[str, dict[str, Any]]] = [] + monkeypatch.setattr(cli_runner.click, 'secho', lambda text, **kwargs: secho_calls.append((text, kwargs))) + + run_with_client(monkeypatch, cli_args, client) + + assert client.connect_calls[-1]['character_set'] == 'utf8' + assert secho_calls == [ + ( + 'Warning: Ignored unknown DSN URI query parameters: unknown.', + {'err': True, 'fg': 'yellow'}, + ) + ] + + def test_run_from_cli_args_reports_missing_dsn_alias_env_var( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -380,6 +444,21 @@ def test_run_from_cli_args_maps_dsn_ssh_jump_parameter(monkeypatch: pytest.Monke assert client.connect_calls[-1]['ssh_jump'] == 'bastion' +def test_run_from_cli_args_does_not_warn_about_known_dsn_query_parameter( + monkeypatch: pytest.MonkeyPatch, +) -> None: + cli_args = make_cli_args() + cli_args.dsn = 'mysql://user@host/db?ssh_jump=bastion' + client = DummyMyCli() + secho_calls: list[tuple[str, dict[str, Any]]] = [] + monkeypatch.setattr(cli_runner.click, 'secho', lambda text, **kwargs: secho_calls.append((text, kwargs))) + + run_with_client(monkeypatch, cli_args, client) + + assert client.connect_calls[-1]['ssh_jump'] == 'bastion' + assert secho_calls == [] + + def test_run_from_cli_args_prefers_cli_ssh_jump_over_dsn_parameter( monkeypatch: pytest.MonkeyPatch, ) -> None: