Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Internal
* Improve test coverage for `packages/special_commands/io_commands.py`.
* Improve test coverage for `packages/execution/sql_execute.py`.
* Improve test coverage for `packages/tabular_output/sql_format.py`.
* Update `pymysql` dependency to v1.2.4.


v2.30.3 (2026/11/05)
Expand Down
5 changes: 2 additions & 3 deletions mycli/client_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import keyring
import pymysql
from pymysql.constants.CR import CR_SERVER_LOST
from pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR
from pymysql.constants.ER import ACCESS_DENIED_ERROR, HANDSHAKE_ERROR, MUST_CHANGE_PASSWORD_LOGIN

from mycli.compat import WIN
from mycli.config import str_to_bool
Expand All @@ -19,7 +19,6 @@
DEFAULT_HOST,
DEFAULT_PORT,
EMPTY_PASSWORD_FLAG_SENTINEL,
ER_MUST_CHANGE_PASSWORD_LOGIN,
)
from mycli.packages.credentials.password_sources import PasswordCandidates
from mycli.packages.execution.sql_execute import SQLExecute
Expand Down Expand Up @@ -433,7 +432,7 @@ def _connect(
keyring_retrieved_cleanly=keyring_retrieved_cleanly,
keyring_save_eligible=keyring_save_eligible,
)
elif e1.args[0] == ER_MUST_CHANGE_PASSWORD_LOGIN:
elif e1.args[0] == MUST_CHANGE_PASSWORD_LOGIN:
self.echo(
"Your password has expired and the server rejected the connection.",
err=True,
Expand Down
4 changes: 0 additions & 4 deletions mycli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@
DEFAULT_WIDTH = 80
DEFAULT_HEIGHT = 25

# MySQL error codes not available in pymysql.constants.ER
ER_MUST_CHANGE_PASSWORD_LOGIN = 1862
ER_MUST_CHANGE_PASSWORD = 1820

EMPTY_PASSWORD_FLAG_SENTINEL = -1
DEFAULT_PROMPT = "\\t \\u@\\h:\\d> "

Expand Down
4 changes: 2 additions & 2 deletions mycli/main_modes/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
CR_SERVER_GONE_ERROR,
CR_SERVER_LOST,
)
from pymysql.constants.ER import MUST_CHANGE_PASSWORD
from pymysql.constants.SERVER_STATUS import SERVER_STATUS_IN_TRANS
from pymysql.cursors import Cursor

Expand All @@ -57,7 +58,6 @@
from mycli.constants import (
DEFAULT_HOST,
DEFAULT_WIDTH,
ER_MUST_CHANGE_PASSWORD,
HOME_URL,
ISSUES_URL,
QueryState,
Expand Down Expand Up @@ -1110,7 +1110,7 @@ def _one_iteration(
mycli.echo(str(exc), err=True, fg='red')
except pymysql.OperationalError as e1:
mycli.logger.debug('Exception: %r', e1)
if e1.args[0] == ER_MUST_CHANGE_PASSWORD:
if e1.args[0] == MUST_CHANGE_PASSWORD:
mycli.sandbox_mode = True
mycli.echo(
"ERROR 1820: You must reset your password using ALTER USER or SET PASSWORD before executing this statement.",
Expand Down
8 changes: 4 additions & 4 deletions mycli/packages/execution/sql_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
from pymysql.connections import Connection
from pymysql.constants import FIELD_TYPE
from pymysql.constants.CR import CR_SSL_CONNECTION_ERROR
from pymysql.constants.ER import MUST_CHANGE_PASSWORD
from pymysql.converters import conversions, convert_date, convert_datetime, convert_time, decoders
from pymysql.cursors import Cursor, SSCursor

from mycli.constants import ER_MUST_CHANGE_PASSWORD
from mycli.packages.execution.background_runner import BackgroundRunner
from mycli.packages.special_commands import io_commands
from mycli.packages.special_commands.main import CommandNotFound, execute
Expand Down Expand Up @@ -296,9 +296,9 @@ def connect(
connect_kwargs['ssl_disabled'] = True
conn = pymysql.connect(**connect_kwargs) # type: ignore[misc]
except pymysql.OperationalError as e:
if e.args[0] == ER_MUST_CHANGE_PASSWORD:
if e.args[0] == MUST_CHANGE_PASSWORD:
# Post-handshake queries (SET NAMES, SET AUTOCOMMIT, init_command)
# fail with ER_MUST_CHANGE_PASSWORD in sandbox mode.
# fail with MUST_CHANGE_PASSWORD in sandbox mode.
# Reconnect with only the raw handshake.
connect_kwargs['defer_connect'] = True
connect_kwargs['autocommit'] = None
Expand Down Expand Up @@ -659,7 +659,7 @@ def _connect_sandbox(conn: Connection) -> None:
"""Connect in sandbox mode, performing only the handshake.

pymysql's normal connect() runs post-handshake queries (SET NAMES,
SET AUTOCOMMIT, init_command) that all fail with ER_MUST_CHANGE_PASSWORD
SET AUTOCOMMIT, init_command) that all fail with MUST_CHANGE_PASSWORD
in sandbox mode. This method performs the raw socket connection and
authentication handshake only.
"""
Expand Down
5 changes: 3 additions & 2 deletions mycli_test/pytests/test_client_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

from configobj import ConfigObj
import pymysql
from pymysql.constants.ER import MUST_CHANGE_PASSWORD_LOGIN
import pytest

from mycli import client_connection
from mycli.client_connection import ClientConnectionMixin
from mycli.constants import DEFAULT_CHARSET, EMPTY_PASSWORD_FLAG_SENTINEL, ER_MUST_CHANGE_PASSWORD_LOGIN
from mycli.constants import DEFAULT_CHARSET, EMPTY_PASSWORD_FLAG_SENTINEL
from mycli.packages.credentials.password_sources import KNOWN_PASSWORD_SOURCES, PasswordCandidates


Expand Down Expand Up @@ -1304,7 +1305,7 @@ def test_connect_exits_when_password_retry_still_has_no_password(monkeypatch: py

def test_connect_reports_expired_password_login_error() -> None:
client = DummyClient()
FakeSQLExecute.effects = [op_error(ER_MUST_CHANGE_PASSWORD_LOGIN, 'expired')]
FakeSQLExecute.effects = [op_error(MUST_CHANGE_PASSWORD_LOGIN, 'expired')]

with pytest.raises(SystemExit) as excinfo:
client.connect(host='db', port=3307)
Expand Down
4 changes: 2 additions & 2 deletions mycli_test/pytests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
to_plain_text,
)
import pymysql
from pymysql.constants.ER import MUST_CHANGE_PASSWORD_LOGIN
from pymysql.err import OperationalError
import pytest

Expand All @@ -40,7 +41,6 @@
DEFAULT_PORT,
DEFAULT_USER,
EMPTY_PASSWORD_FLAG_SENTINEL,
ER_MUST_CHANGE_PASSWORD_LOGIN,
TEST_DATABASE,
)
from mycli.main import (
Expand Down Expand Up @@ -2386,7 +2386,7 @@ def test_connect_reports_expired_password_login_error(monkeypatch: pytest.Monkey

class ExpiredPasswordSQLExecute(RecordingSQLExecute):
calls: list[dict[str, Any]] = []
side_effects: list[Any] = [pymysql.OperationalError(ER_MUST_CHANGE_PASSWORD_LOGIN, 'must change password')]
side_effects: list[Any] = [pymysql.OperationalError(MUST_CHANGE_PASSWORD_LOGIN, 'must change password')]

monkeypatch.setattr(mycli.client_connection, 'SQLExecute', ExpiredPasswordSQLExecute)

Expand Down
2 changes: 1 addition & 1 deletion mycli_test/pytests/test_main_modes_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -2238,7 +2238,7 @@ class FakeSQLExecute:
connection_id = 0

def run(self, text: str) -> Iterator[SQLResult]:
raise pymysql.OperationalError(repl_mode.ER_MUST_CHANGE_PASSWORD, 'must change password')
raise pymysql.OperationalError(repl_mode.MUST_CHANGE_PASSWORD, 'must change password')

cli = make_repl_cli(FakeSQLExecute())

Expand Down
2 changes: 1 addition & 1 deletion mycli_test/pytests/test_sql_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@ def test_connect_enters_sandbox_after_ssl_fallback(monkeypatch: pytest.MonkeyPat
connect = Mock(
side_effect=[
pymysql.OperationalError(sql_execute.CR_SSL_CONNECTION_ERROR, 'SSL unsupported'),
pymysql.OperationalError(sql_execute.ER_MUST_CHANGE_PASSWORD, 'must change password'),
pymysql.OperationalError(sql_execute.MUST_CHANGE_PASSWORD, 'must change password'),
new_conn,
]
)
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies = [
"cryptography ~= 50.0.2",
"Pygments ~= 2.21.0",
"prompt_toolkit ~= 3.0.53",
"PyMySQL ~= 1.2.3",
"PyMySQL ~= 1.2.4",
"sqlparse ~= 0.6.0",
"sqlglot ~= 30.25.0",
"sqlglotc ~= 30.25.0",
Expand Down Expand Up @@ -101,6 +101,7 @@ exclude-newer = '1 week'

[tool.uv.exclude-newer-package]
cli_helpers = false
pymysql = false # removeme 2026-11-16

[tool.ruff]
target-version = 'py311'
Expand Down
Loading