From 2b6633cb3805752314bd73994cd1f034f6abea06 Mon Sep 17 00:00:00 2001 From: Phillip Cloud <417981+cpcloud@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:43:32 -0400 Subject: [PATCH] chore(mysql): port to MySQLdb instead of pymysql (#10077) Replaces pymysql with mysqlclient, mostly out of frustration with bizarre GC behavior discovered during #10055. I think this is probably a breaking change due to some changes in how types are inferred for JSON, INET and UUID types. BREAKING CHANGE: Ibis now uses the `MySQLdb` driver. You may need to install MySQL client libraries to **build** the extension. --- .github/renovate.json | 2 +- .github/workflows/ibis-backends.yml | 2 + conda/environment-arm64-flink.yml | 2 +- conda/environment-arm64.yml | 2 +- conda/environment.yml | 2 +- ibis/backends/mysql/__init__.py | 158 +++++++++++------------ ibis/backends/mysql/datatypes.py | 47 +++---- ibis/backends/mysql/tests/conftest.py | 2 +- ibis/backends/mysql/tests/test_client.py | 6 +- ibis/backends/tests/errors.py | 6 +- ibis/backends/tests/test_client.py | 5 - poetry.lock | 37 +++--- pyproject.toml | 4 +- requirements-dev.txt | 2 +- 14 files changed, 136 insertions(+), 141 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 245cad02c651..3dad82e3ecb1 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -67,7 +67,7 @@ "addLabels": ["druid"] }, { - "matchPackagePatterns": ["pymysql", "mariadb"], + "matchPackagePatterns": ["mysqlclient", "mariadb"], "addLabels": ["mysql"] }, { diff --git a/.github/workflows/ibis-backends.yml b/.github/workflows/ibis-backends.yml index 1a120a80b816..d44fbd0b0c48 100644 --- a/.github/workflows/ibis-backends.yml +++ b/.github/workflows/ibis-backends.yml @@ -141,6 +141,7 @@ jobs: - polars sys-deps: - libgeos-dev + - default-libmysqlclient-dev - name: postgres title: PostgreSQL extras: @@ -271,6 +272,7 @@ jobs: - mysql sys-deps: - libgeos-dev + - default-libmysqlclient-dev - os: windows-latest backend: name: clickhouse diff --git a/conda/environment-arm64-flink.yml b/conda/environment-arm64-flink.yml index c444cc2e34c0..4a4a45d1c3c6 100644 --- a/conda/environment-arm64-flink.yml +++ b/conda/environment-arm64-flink.yml @@ -33,7 +33,7 @@ dependencies: - pyarrow-hotfix >=0.4 - pydata-google-auth - pydruid >=0.6.5 - - pymysql >=1 + - mysqlclient >=2.2.4 - pyspark >=3 - python-dateutil >=2.8.2 - python-duckdb >=0.8.1 diff --git a/conda/environment-arm64.yml b/conda/environment-arm64.yml index 6a4bde8f8b10..6a6ce8943e85 100644 --- a/conda/environment-arm64.yml +++ b/conda/environment-arm64.yml @@ -33,7 +33,7 @@ dependencies: - pyarrow-hotfix >=0.4 - pydata-google-auth - pydruid >=0.6.5 - - pymysql >=1 + - mysqlclient >=2.2.4 - pyodbc >=4.0.39 - pyspark >=3 - python-dateutil >=2.8.2 diff --git a/conda/environment.yml b/conda/environment.yml index fb52c768b17e..9aae1e79f974 100644 --- a/conda/environment.yml +++ b/conda/environment.yml @@ -33,7 +33,7 @@ dependencies: - pyarrow-hotfix >=0.4 - pydata-google-auth - pydruid >=0.6.5 - - pymysql >=1 + - mysqlclient >=2.2.4 - pyodbc >=4.0.39 - pyspark >=3 - python >=3.10 diff --git a/ibis/backends/mysql/__init__.py b/ibis/backends/mysql/__init__.py index 664a1eff1da8..c477c191ffb7 100644 --- a/ibis/backends/mysql/__init__.py +++ b/ibis/backends/mysql/__init__.py @@ -3,18 +3,17 @@ from __future__ import annotations import contextlib -import re import warnings from functools import cached_property from operator import itemgetter from typing import TYPE_CHECKING, Any from urllib.parse import unquote_plus -import pymysql +import MySQLdb import sqlglot as sg import sqlglot.expressions as sge -from pymysql.constants import ER -from pymysql.err import ProgrammingError +from MySQLdb import ProgrammingError +from MySQLdb.constants import ER import ibis import ibis.backends.sql.compilers as sc @@ -24,7 +23,6 @@ import ibis.expr.types as ir from ibis import util from ibis.backends import CanCreateDatabase -from ibis.backends.mysql.datatypes import _type_from_cursor_info from ibis.backends.sql import SQLBackend from ibis.backends.sql.compilers.base import STAR, TRUE, C @@ -89,8 +87,7 @@ def _from_url(self, url: ParseResult, **kwargs): @cached_property def version(self): - matched = re.search(r"(\d+)\.(\d+)\.(\d+)", self.con.server_version) - return ".".join(matched.groups()) + return ".".join(map(str, self.con._server_version)) def do_connect( self, @@ -98,7 +95,6 @@ def do_connect( user: str | None = None, password: str | None = None, port: int = 3306, - database: str | None = None, autocommit: bool = True, **kwargs, ) -> None: @@ -114,12 +110,10 @@ def do_connect( Password port Port - database - Database to connect to autocommit Autocommit mode kwargs - Additional keyword arguments passed to `pymysql.connect` + Additional keyword arguments passed to `MySQLdb.connect` Examples -------- @@ -149,14 +143,12 @@ def do_connect( year int32 month int32 """ - self.con = pymysql.connect( + self.con = MySQLdb.connect( user=user, - host=host, + host="127.0.0.1" if host == "localhost" else host, port=port, password=password, - database=database, autocommit=autocommit, - conv=pymysql.converters.conversions, **kwargs, ) @@ -164,7 +156,7 @@ def do_connect( @util.experimental @classmethod - def from_connection(cls, con: pymysql.Connection) -> Backend: + def from_connection(cls, con) -> Backend: """Create an Ibis client from an existing connection to a MySQL database. Parameters @@ -179,7 +171,7 @@ def from_connection(cls, con: pymysql.Connection) -> Backend: return new_backend def _post_connect(self) -> None: - with contextlib.closing(self.con.cursor()) as cur: + with self.con.cursor() as cur: try: cur.execute("SET @@session.time_zone = 'UTC'") except Exception as e: # noqa: BLE001 @@ -198,23 +190,34 @@ def list_databases(self, like: str | None = None) -> list[str]: return self._filter_with_like(databases, like) def _get_schema_using_query(self, query: str) -> sch.Schema: - with self.begin() as cur: - cur.execute( - sg.select(STAR) - .from_( - sg.parse_one(query, dialect=self.dialect).subquery( - sg.to_identifier("tmp", quoted=self.compiler.quoted) - ) + from ibis.backends.mysql.datatypes import _type_from_cursor_info + + sql = ( + sg.select(STAR) + .from_( + sg.parse_one(query, dialect=self.dialect).subquery( + sg.to_identifier("tmp", quoted=self.compiler.quoted) ) - .limit(0) - .sql(self.dialect) ) - return sch.Schema( - { - field.name: _type_from_cursor_info(descr, field) - for descr, field in zip(cur.description, cur._result.fields) - } + .limit(0) + .sql(self.dialect) + ) + with self.begin() as cur: + cur.execute(sql) + descr, flags = cur.description, cur.description_flags + + items = {} + for (name, type_code, _, _, field_length, scale, _), raw_flags in zip( + descr, flags + ): + item = _type_from_cursor_info( + flags=raw_flags, + type_code=type_code, + field_length=field_length, + scale=scale, ) + items[name] = item + return sch.Schema(items) def get_schema( self, name: str, *, catalog: str | None = None, database: str | None = None @@ -258,13 +261,20 @@ def drop_database(self, name: str, force: bool = False) -> None: def begin(self): con = self.con cur = con.cursor() + autocommit = con.get_autocommit() + + if not autocommit: + con.begin() + try: yield cur except Exception: - con.rollback() + if not autocommit: + con.rollback() raise else: - con.commit() + if not autocommit: + con.commit() finally: cur.close() @@ -272,7 +282,7 @@ def begin(self): # from .execute() @contextlib.contextmanager def _safe_raw_sql(self, *args, **kwargs): - with contextlib.closing(self.raw_sql(*args, **kwargs)) as result: + with self.raw_sql(*args, **kwargs) as result: yield result def raw_sql(self, query: str | sg.Expression, **kwargs: Any) -> Any: @@ -280,16 +290,23 @@ def raw_sql(self, query: str | sg.Expression, **kwargs: Any) -> Any: query = query.sql(dialect=self.name) con = self.con + autocommit = con.get_autocommit() + cursor = con.cursor() + if not autocommit: + con.begin() + try: cursor.execute(query, **kwargs) except Exception: - con.rollback() + if not autocommit: + con.rollback() cursor.close() raise else: - con.commit() + if not autocommit: + con.commit() return cursor # TODO: disable positional arguments @@ -406,11 +423,9 @@ def create_table( if temp: properties.append(sge.TemporaryProperty()) - temp_memtable_view = None if obj is not None: if not isinstance(obj, ir.Expr): table = ibis.memtable(obj) - temp_memtable_view = table.op().name else: table = obj @@ -428,39 +443,33 @@ def create_table( if not schema: schema = table.schema() - table_expr = sg.table(temp_name, catalog=database, quoted=self.compiler.quoted) - target = sge.Schema( - this=table_expr, expressions=schema.to_sqlglot(self.dialect) - ) + quoted = self.compiler.quoted + dialect = self.dialect + + table_expr = sg.table(temp_name, catalog=database, quoted=quoted) + target = sge.Schema(this=table_expr, expressions=schema.to_sqlglot(dialect)) create_stmt = sge.Create( - kind="TABLE", - this=target, - properties=sge.Properties(expressions=properties), + kind="TABLE", this=target, properties=sge.Properties(expressions=properties) ) - this = sg.table(name, catalog=database, quoted=self.compiler.quoted) + this = sg.table(name, catalog=database, quoted=quoted) with self._safe_raw_sql(create_stmt) as cur: if query is not None: - insert_stmt = sge.Insert(this=table_expr, expression=query).sql( - self.name - ) - cur.execute(insert_stmt) + cur.execute(sge.Insert(this=table_expr, expression=query).sql(dialect)) if overwrite: + cur.execute(sge.Drop(kind="TABLE", this=this, exists=True).sql(dialect)) cur.execute( - sge.Drop(kind="TABLE", this=this, exists=True).sql(self.name) - ) - cur.execute( - f"ALTER TABLE IF EXISTS {table_expr.sql(self.name)} RENAME TO {this.sql(self.name)}" + sge.Alter( + kind="TABLE", + this=table_expr, + exists=True, + actions=[sge.RenameTable(this=this)], + ).sql(dialect) ) if schema is None: - # Clean up temporary memtable if we've created one - # for in-memory reads - if temp_memtable_view is not None: - self.drop_table(temp_memtable_view) - return self.table(name, database=database) # preserve the input schema if it was provided @@ -477,7 +486,7 @@ def _in_memory_table_exists(self, name: str) -> bool: with self.begin() as cur: cur.execute(sql) cur.fetchall() - except pymysql.err.ProgrammingError as e: + except MySQLdb.ProgrammingError as e: err_code, _ = e.args if err_code == ER.NO_SUCH_TABLE: return False @@ -495,16 +504,17 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None: name = op.name quoted = self.compiler.quoted + dialect = self.dialect create_stmt = sg.exp.Create( kind="TABLE", this=sg.exp.Schema( this=sg.to_identifier(name, quoted=quoted), - expressions=schema.to_sqlglot(self.dialect), + expressions=schema.to_sqlglot(dialect), ), properties=sg.exp.Properties(expressions=[sge.TemporaryProperty()]), ) - create_stmt_sql = create_stmt.sql(self.name) + create_stmt_sql = create_stmt.sql(dialect) df = op.data.to_frame() # nan can not be used with MySQL @@ -549,23 +559,7 @@ def _fetch_from_cursor(self, cursor, schema: sch.Schema) -> pd.DataFrame: from ibis.backends.mysql.converter import MySQLPandasData - try: - df = pd.DataFrame.from_records( - cursor, columns=schema.names, coerce_float=True - ) - except Exception: - # clean up the cursor if we fail to create the DataFrame - # - # in the sqlite case failing to close the cursor results in - # artificially locked tables - cursor.close() - raise - df = MySQLPandasData.convert_table(df, schema) - return df - - def _finalize_memtable(self, name: str) -> None: - """No-op. - - Executing **any** SQL in a finalizer causes the underlying connection - socket to be set to `None`. It is unclear why this happens. - """ + df = pd.DataFrame.from_records( + cursor.fetchall(), columns=schema.names, coerce_float=True + ) + return MySQLPandasData.convert_table(df, schema) diff --git a/ibis/backends/mysql/datatypes.py b/ibis/backends/mysql/datatypes.py index 414a758ce38c..f758f0288197 100644 --- a/ibis/backends/mysql/datatypes.py +++ b/ibis/backends/mysql/datatypes.py @@ -3,16 +3,24 @@ import inspect from functools import partial -from pymysql.constants import FIELD_TYPE +from MySQLdb.constants import FIELD_TYPE, FLAG import ibis.expr.datatypes as dt -# binary character set -# used to distinguish blob binary vs blob text -MY_CHARSET_BIN = 63 +TEXT_TYPES = ( + FIELD_TYPE.BIT, + FIELD_TYPE.BLOB, + FIELD_TYPE.LONG_BLOB, + FIELD_TYPE.MEDIUM_BLOB, + FIELD_TYPE.STRING, + FIELD_TYPE.TINY_BLOB, + FIELD_TYPE.VAR_STRING, + FIELD_TYPE.VARCHAR, + FIELD_TYPE.GEOMETRY, +) -def _type_from_cursor_info(descr, field) -> dt.DataType: +def _type_from_cursor_info(*, flags, type_code, field_length, scale) -> dt.DataType: """Construct an ibis type from MySQL field descr and field result metadata. This method is complex because the MySQL protocol is complex. @@ -24,19 +32,14 @@ def _type_from_cursor_info(descr, field) -> dt.DataType: strings, because the protocol does not appear to preserve the logical type, only the physical type. """ - from pymysql.connections import TEXT_TYPES - - _, type_code, _, _, field_length, scale, _ = descr - flags = _FieldFlags(field.flags) + flags = _FieldFlags(flags) typename = _type_codes.get(type_code) if typename is None: raise NotImplementedError(f"MySQL type code {type_code:d} is not supported") if typename in ("DECIMAL", "NEWDECIMAL"): precision = _decimal_length_to_precision( - length=field_length, - scale=scale, - is_unsigned=flags.is_unsigned, + length=field_length, scale=scale, is_unsigned=flags.is_unsigned ) typ = partial(_type_mapping[typename], precision=precision, scale=scale) elif typename == "BIT": @@ -54,8 +57,7 @@ def _type_from_cursor_info(descr, field) -> dt.DataType: # sets are limited to strings typ = dt.Array(dt.string) elif type_code in TEXT_TYPES: - # binary text - if field.charsetnr == MY_CHARSET_BIN: + if flags.is_binary: typ = dt.Binary else: typ = dt.String @@ -115,11 +117,6 @@ class _FieldFlags: is a primary key or not. """ - UNSIGNED = 1 << 5 - TIMESTAMP = 1 << 10 - SET = 1 << 11 - NUM = 1 << 15 - __slots__ = ("value",) def __init__(self, value: int) -> None: @@ -127,16 +124,20 @@ def __init__(self, value: int) -> None: @property def is_unsigned(self) -> bool: - return (self.UNSIGNED & self.value) != 0 + return (FLAG.UNSIGNED & self.value) != 0 @property def is_timestamp(self) -> bool: - return (self.TIMESTAMP & self.value) != 0 + return (FLAG.TIMESTAMP & self.value) != 0 @property def is_set(self) -> bool: - return (self.SET & self.value) != 0 + return (FLAG.SET & self.value) != 0 @property def is_num(self) -> bool: - return (self.NUM & self.value) != 0 + return (FLAG.NUM & self.value) != 0 + + @property + def is_binary(self) -> bool: + return (FLAG.BINARY & self.value) != 0 diff --git a/ibis/backends/mysql/tests/conftest.py b/ibis/backends/mysql/tests/conftest.py index f7c463048767..f343dd2aa211 100644 --- a/ibis/backends/mysql/tests/conftest.py +++ b/ibis/backends/mysql/tests/conftest.py @@ -29,7 +29,7 @@ class TestConf(ServiceBackendTest): supports_structs = False rounding_method = "half_to_even" service_name = "mysql" - deps = ("pymysql",) + deps = ("MySQLdb",) @property def test_files(self) -> Iterable[Path]: diff --git a/ibis/backends/mysql/tests/test_client.py b/ibis/backends/mysql/tests/test_client.py index 456dace97e57..95f4bd048c8e 100644 --- a/ibis/backends/mysql/tests/test_client.py +++ b/ibis/backends/mysql/tests/test_client.py @@ -57,7 +57,6 @@ param("bit(17)", dt.int32, id="bit_17"), param("bit(33)", dt.int64, id="bit_33"), # mariadb doesn't have a distinct json type - param("json", dt.string, id="json"), param("enum('small', 'medium', 'large')", dt.string, id="enum"), param("set('a', 'b', 'c', 'd')", dt.Array(dt.string), id="set"), param("mediumblob", dt.binary, id="mediumblob"), @@ -93,8 +92,9 @@ def test_get_schema_from_query(con, mysql_type, expected_type): @pytest.mark.parametrize( ("mysql_type", "get_schema_expected_type", "table_expected_type"), [ - param("inet6", dt.string, dt.inet, id="inet"), - param("uuid", dt.string, dt.uuid, id="uuid"), + param("json", dt.binary, dt.string, id="json"), + param("inet6", dt.binary, dt.inet, id="inet"), + param("uuid", dt.binary, dt.uuid, id="uuid"), ], ) def test_get_schema_from_query_special_cases( diff --git a/ibis/backends/tests/errors.py b/ibis/backends/tests/errors.py index 05eabee76d1d..cc21b1582850 100644 --- a/ibis/backends/tests/errors.py +++ b/ibis/backends/tests/errors.py @@ -121,9 +121,9 @@ ) = PsycoPg2UndefinedObject = None try: - from pymysql.err import NotSupportedError as MySQLNotSupportedError - from pymysql.err import OperationalError as MySQLOperationalError - from pymysql.err import ProgrammingError as MySQLProgrammingError + from MySQLdb import NotSupportedError as MySQLNotSupportedError + from MySQLdb import OperationalError as MySQLOperationalError + from MySQLdb import ProgrammingError as MySQLProgrammingError except ImportError: MySQLNotSupportedError = MySQLProgrammingError = MySQLOperationalError = None diff --git a/ibis/backends/tests/test_client.py b/ibis/backends/tests/test_client.py index 8e3618b4454b..57943042daf6 100644 --- a/ibis/backends/tests/test_client.py +++ b/ibis/backends/tests/test_client.py @@ -1709,11 +1709,6 @@ def test_insert_into_table_missing_columns(con, temp_table): raises=AssertionError, reason="memtables are assembled every time", ) -@pytest.mark.notyet( - ["mysql"], - raises=AssertionError, - reason="can't execute SQL inside of a finalizer without breaking everything", -) @pytest.mark.notyet( ["bigquery"], raises=AssertionError, reason="test is flaky", strict=False ) diff --git a/poetry.lock b/poetry.lock index 96e4b251d42a..4a6eb4a87f13 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3825,6 +3825,24 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "mysqlclient" +version = "2.2.4" +description = "Python interface to MySQL" +optional = true +python-versions = ">=3.8" +files = [ + {file = "mysqlclient-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac44777eab0a66c14cb0d38965572f762e193ec2e5c0723bcd11319cc5b693c5"}, + {file = "mysqlclient-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:329e4eec086a2336fe3541f1ce095d87a6f169d1cc8ba7b04ac68bcb234c9711"}, + {file = "mysqlclient-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:e1ebe3f41d152d7cb7c265349fdb7f1eca86ccb0ca24a90036cde48e00ceb2ab"}, + {file = "mysqlclient-2.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:3c318755e06df599338dad7625f884b8a71fcf322a9939ef78c9b3db93e1de7a"}, + {file = "mysqlclient-2.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:9d4c015480c4a6b2b1602eccd9846103fc70606244788d04aa14b31c4bd1f0e2"}, + {file = "mysqlclient-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d43987bb9626096a302ca6ddcdd81feaeca65ced1d5fe892a6a66b808326aa54"}, + {file = "mysqlclient-2.2.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4e80dcad884dd6e14949ac6daf769123223a52a6805345608bf49cdaf7bc8b3a"}, + {file = "mysqlclient-2.2.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9d3310295cb682232cadc28abd172f406c718b9ada41d2371259098ae37779d3"}, + {file = "mysqlclient-2.2.4.tar.gz", hash = "sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41"}, +] + [[package]] name = "narwhals" version = "1.8.1" @@ -5262,21 +5280,6 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] -[[package]] -name = "pymysql" -version = "1.1.1" -description = "Pure Python MySQL Driver" -optional = true -python-versions = ">=3.7" -files = [ - {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, - {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, -] - -[package.extras] -ed25519 = ["PyNaCl (>=1.4.0)"] -rsa = ["cryptography"] - [[package]] name = "pyodbc" version = "5.1.0" @@ -7880,7 +7883,7 @@ flink = ["numpy", "pandas", "pyarrow", "pyarrow-hotfix", "rich"] geospatial = ["geoarrow-types", "geopandas", "pyproj", "shapely"] impala = ["impyla", "numpy", "pandas", "pyarrow", "pyarrow-hotfix", "rich"] mssql = ["numpy", "pandas", "pyarrow", "pyarrow-hotfix", "pyodbc", "rich"] -mysql = ["numpy", "pandas", "pyarrow", "pyarrow-hotfix", "pymysql", "rich"] +mysql = ["mysqlclient", "numpy", "pandas", "pyarrow", "pyarrow-hotfix", "rich"] oracle = ["numpy", "oracledb", "packaging", "pandas", "pyarrow", "pyarrow-hotfix", "rich"] polars = ["numpy", "packaging", "pandas", "polars", "pyarrow", "pyarrow-hotfix", "rich"] postgres = ["numpy", "pandas", "psycopg2", "pyarrow", "pyarrow-hotfix", "rich"] @@ -7894,4 +7897,4 @@ visualization = ["graphviz"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "82384564308f7be107e844339bbcaaf90643f677702f39cf19231f121d2bc7f5" +content-hash = "7acc0d4effb3c7f5476d41411f0a84982ab618fde1912ff6a32092b5c14787bf" diff --git a/pyproject.toml b/pyproject.toml index f2fa45d43ac9..f47c0574b714 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ psycopg2 = { version = ">=2.8.4,<3", optional = true } pydata-google-auth = { version = ">=1.4.0,<2", optional = true } pydruid = { version = ">=0.6.7,<1", optional = true } pyexasol = { version = ">=0.25.2,<1", optional = true, extras = ["pandas"] } -pymysql = { version = ">=1,<2", optional = true } +mysqlclient = { version = ">=2.2.4,<3", optional = true } pyodbc = { version = ">=4.0.39,<6", optional = true } pyspark = { version = ">=3.3.3,<4", optional = true } # used to support posix regexen in the pandas and sqlite backends @@ -184,7 +184,7 @@ exasol = ["pyexasol", "pyarrow", "pyarrow-hotfix", "numpy", "pandas", "rich"] flink = ["pyarrow", "pyarrow-hotfix", "numpy", "pandas", "rich"] impala = ["impyla", "pyarrow", "pyarrow-hotfix", "numpy", "pandas", "rich"] mssql = ["pyodbc", "pyarrow", "pyarrow-hotfix", "numpy", "pandas", "rich"] -mysql = ["pymysql", "pyarrow", "pyarrow-hotfix", "numpy", "pandas", "rich"] +mysql = ["mysqlclient", "pyarrow", "pyarrow-hotfix", "numpy", "pandas", "rich"] oracle = [ "oracledb", "packaging", diff --git a/requirements-dev.txt b/requirements-dev.txt index 28f9348419d3..44fb62df156c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -137,6 +137,7 @@ more-itertools==10.5.0 ; python_version >= "3.10" and python_version < "4.0" msgpack==1.1.0 ; python_version >= "3.10" and python_version < "4.0" multidict==6.1.0 ; python_version >= "3.10" and python_version < "4.0" mypy-extensions==1.0.0 ; python_version >= "3.10" and python_version < "4.0" +mysqlclient==2.2.4 ; python_version >= "3.10" and python_version < "4.0" narwhals==1.8.1 ; python_version >= "3.10" and python_version < "3.13" nbclient==0.10.0 ; python_version >= "3.10" and python_version < "3.13" nbconvert==7.16.4 ; python_version >= "3.10" and python_version < "3.13" @@ -198,7 +199,6 @@ pyexasol[pandas]==0.27.0 ; python_version >= "3.10" and python_version < "4.0" pygments==2.18.0 ; python_version >= "3.10" and python_version < "4.0" pyinstrument==4.7.3 ; python_version >= "3.10" and python_version < "4.0" pyjwt==2.9.0 ; python_version >= "3.10" and python_version < "4.0" -pymysql==1.1.1 ; python_version >= "3.10" and python_version < "4.0" pyodbc==5.1.0 ; python_version >= "3.10" and python_version < "4.0" pyogrio==0.9.0 ; python_version >= "3.10" and python_version < "4.0" pyopenssl==24.2.1 ; python_version >= "3.10" and python_version < "4.0"