Skip to content

Commit 6262cc2

Browse files
authored
✨ add MySQL 8.4 and MariaDB 11.4 support (#85)
1 parent 17b36ee commit 6262cc2

File tree

12 files changed

+217
-21
lines changed

12 files changed

+217
-21
lines changed

.github/workflows/test.yml

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,36 @@ jobs:
306306
experimental: false
307307
py: "3.12"
308308

309+
- toxenv: "python3.8"
310+
db: "mariadb:11.4"
311+
legacy_db: 0
312+
experimental: false
313+
py: "3.8"
314+
315+
- toxenv: "python3.9"
316+
db: "mariadb:11.4"
317+
legacy_db: 0
318+
experimental: false
319+
py: "3.9"
320+
321+
- toxenv: "python3.10"
322+
db: "mariadb:11.4"
323+
legacy_db: 0
324+
experimental: false
325+
py: "3.10"
326+
327+
- toxenv: "python3.11"
328+
db: "mariadb:11.4"
329+
legacy_db: 0
330+
experimental: false
331+
py: "3.11"
332+
333+
- toxenv: "python3.12"
334+
db: "mariadb:11.4"
335+
legacy_db: 0
336+
experimental: false
337+
py: "3.12"
338+
309339
- toxenv: "python3.8"
310340
db: "mysql:5.5"
311341
legacy_db: 1
@@ -425,15 +455,46 @@ jobs:
425455
legacy_db: 0
426456
experimental: false
427457
py: "3.12"
458+
459+
- toxenv: "python3.8"
460+
db: "mysql:8.4"
461+
legacy_db: 0
462+
experimental: true
463+
py: "3.8"
464+
465+
- toxenv: "python3.9"
466+
db: "mysql:8.4"
467+
legacy_db: 0
468+
experimental: true
469+
py: "3.9"
470+
471+
- toxenv: "python3.10"
472+
db: "mysql:8.4"
473+
legacy_db: 0
474+
experimental: true
475+
py: "3.10"
476+
477+
- toxenv: "python3.11"
478+
db: "mysql:8.4"
479+
legacy_db: 0
480+
experimental: true
481+
py: "3.11"
482+
483+
- toxenv: "python3.12"
484+
db: "mysql:8.4"
485+
legacy_db: 0
486+
experimental: true
487+
py: "3.12"
428488
continue-on-error: ${{ matrix.experimental }}
429489
services:
430490
mysql:
431-
image: "${{ matrix.db }}"
491+
image: ${{ matrix.db }}
432492
ports:
433493
- 3306:3306
434494
env:
435495
MYSQL_ALLOW_EMPTY_PASSWORD: yes
436-
options: "--name=mysqld"
496+
options: >-
497+
--name=mysqld
437498
steps:
438499
- uses: actions/checkout@v4
439500
- name: Set up Python ${{ matrix.py }}
@@ -462,31 +523,52 @@ jobs:
462523
MYSQL_PORT: 3306
463524
run: |
464525
set -e
526+
465527
while :
466528
do
467529
sleep 1
468530
mysql -h127.0.0.1 -uroot -e 'select version()' && break
469531
done
532+
533+
case "$DB" in
534+
'mysql:8.0'|'mysql:8.4')
535+
mysql -h127.0.0.1 -uroot -e "SET GLOBAL local_infile=on"
536+
docker cp mysqld:/var/lib/mysql/public_key.pem "${HOME}"
537+
docker cp mysqld:/var/lib/mysql/ca.pem "${HOME}"
538+
docker cp mysqld:/var/lib/mysql/server-cert.pem "${HOME}"
539+
docker cp mysqld:/var/lib/mysql/client-key.pem "${HOME}"
540+
docker cp mysqld:/var/lib/mysql/client-cert.pem "${HOME}"
541+
;;
542+
esac
543+
544+
USER_CREATION_COMMANDS=''
545+
WITH_PLUGIN=''
546+
470547
if [ "$DB" == 'mysql:8.0' ]; then
471548
WITH_PLUGIN='with mysql_native_password'
472-
mysql -h127.0.0.1 -uroot -e "SET GLOBAL local_infile=on"
473-
docker cp mysqld:/var/lib/mysql/public_key.pem "${HOME}"
474-
docker cp mysqld:/var/lib/mysql/ca.pem "${HOME}"
475-
docker cp mysqld:/var/lib/mysql/server-cert.pem "${HOME}"
476-
docker cp mysqld:/var/lib/mysql/client-key.pem "${HOME}"
477-
docker cp mysqld:/var/lib/mysql/client-cert.pem "${HOME}"
478-
mysql -uroot -h127.0.0.1 -e '
549+
USER_CREATION_COMMANDS='
479550
CREATE USER
480551
user_sha256 IDENTIFIED WITH "sha256_password" BY "pass_sha256",
481552
nopass_sha256 IDENTIFIED WITH "sha256_password",
482553
user_caching_sha2 IDENTIFIED WITH "caching_sha2_password" BY "pass_caching_sha2",
483554
nopass_caching_sha2 IDENTIFIED WITH "caching_sha2_password"
484-
PASSWORD EXPIRE NEVER;'
485-
mysql -uroot -h127.0.0.1 -e 'GRANT RELOAD ON *.* TO user_caching_sha2;'
486-
else
487-
WITH_PLUGIN=''
555+
PASSWORD EXPIRE NEVER;
556+
GRANT RELOAD ON *.* TO user_caching_sha2;'
557+
elif [ "$DB" == 'mysql:8.4' ]; then
558+
WITH_PLUGIN='with caching_sha2_password'
559+
USER_CREATION_COMMANDS='
560+
CREATE USER
561+
user_caching_sha2 IDENTIFIED WITH "caching_sha2_password" BY "pass_caching_sha2",
562+
nopass_caching_sha2 IDENTIFIED WITH "caching_sha2_password"
563+
PASSWORD EXPIRE NEVER;
564+
GRANT RELOAD ON *.* TO user_caching_sha2;'
565+
fi
566+
567+
if [ ! -z "$USER_CREATION_COMMANDS" ]; then
568+
mysql -uroot -h127.0.0.1 -e "$USER_CREATION_COMMANDS"
488569
fi
489-
mysql -h127.0.0.1 -uroot -e "create database $MYSQL_DATABASE DEFAULT CHARACTER SET utf8mb4"
570+
571+
mysql -h127.0.0.1 -uroot -e "create database $MYSQL_DATABASE DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
490572
mysql -h127.0.0.1 -uroot -e "create user $MYSQL_USER identified $WITH_PLUGIN by '${MYSQL_PASSWORD}'; grant all on ${MYSQL_DATABASE}.* to ${MYSQL_USER};"
491573
mysql -h127.0.0.1 -uroot -e "create user ${MYSQL_USER}@localhost identified $WITH_PLUGIN by '${MYSQL_PASSWORD}'; grant all on ${MYSQL_DATABASE}.* to ${MYSQL_USER}@localhost;"
492574
- name: Create db_credentials.json

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[![PyPI](https://img.shields.io/pypi/v/mysql-to-sqlite3)](https://pypi.org/project/mysql-to-sqlite3/)
22
[![PyPI - Downloads](https://img.shields.io/pypi/dm/mysql-to-sqlite3)](https://pypistats.org/packages/mysql-to-sqlite3)
33
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mysql-to-sqlite3)](https://pypi.org/project/mysql-to-sqlite3/)
4-
[![MySQL Support](https://img.shields.io/static/v1?label=MySQL&message=5.5+|+5.6+|+5.7+|+8.0&color=2b5d80)](https://img.shields.io/static/v1?label=MySQL&message=5.6+|+5.7+|+8.0&color=2b5d80)
5-
[![MariaDB Support](https://img.shields.io/static/v1?label=MariaDB&message=5.5+|+10.0+|+10.1+|+10.2+|+10.3+|+10.4+|+10.5+|+10.6|+10.11&color=C0765A)](https://img.shields.io/static/v1?label=MariaDB&message=10.0+|+10.1+|+10.2+|+10.3+|+10.4+|+10.5&color=C0765A)
4+
[![MySQL Support](https://img.shields.io/static/v1?label=MySQL&message=5.5+|+5.6+|+5.7+|+8.0+|+8.4&color=2b5d80)](https://img.shields.io/static/v1?label=MySQL&message=5.5+|+5.6+|+5.7+|+8.0+|+8.4&color=2b5d80)
5+
[![MariaDB Support](https://img.shields.io/static/v1?label=MariaDB&message=5.5+|+10.0+|+10.1+|+10.2+|+10.3+|+10.4+|+10.5+|+10.6|+10.11+|+11.4&color=C0765A)](https://img.shields.io/static/v1?label=MariaDB&message=5.5|+10.0+|+10.1+|+10.2+|+10.3+|+10.4+|+10.5|+11.4&color=C0765A)
66
[![GitHub license](https://img.shields.io/github/license/techouse/mysql-to-sqlite3)](https://github.com/techouse/mysql-to-sqlite3/blob/master/LICENSE)
77
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE-OF-CONDUCT.md)
88
[![PyPI - Format](https://img.shields.io/pypi/format/mysql-to-sqlite3)](https://pypi.org/project/sqlite3-to-mysql/)
@@ -31,8 +31,6 @@ mysql2sqlite --help
3131
```
3232
Usage: mysql2sqlite [OPTIONS]
3333
34-
mysql2sqlite version 2.1.12 Copyright (c) 2019-2024 Klemen Tusar
35-
3634
Options:
3735
-f, --sqlite-file PATH SQLite3 database file [required]
3836
-d, --mysql-database TEXT MySQL database name [required]
@@ -64,6 +62,9 @@ Options:
6462
-W, --without-data Do not transfer table data, DDL only.
6563
-h, --mysql-host TEXT MySQL host. Defaults to localhost.
6664
-P, --mysql-port INTEGER MySQL port. Defaults to 3306.
65+
--mysql-charset TEXT MySQL database and table character set
66+
[default: utf8mb4]
67+
--mysql-collation TEXT MySQL database and table collation
6768
-S, --skip-ssl Disable MySQL connection encryption.
6869
-c, --chunk INTEGER Chunk reading/writing SQL records
6970
-l, --log-file PATH Log file

docs/README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Connection Options
4444

4545
- ``-h, --mysql-host TEXT``: MySQL host. Defaults to localhost.
4646
- ``-P, --mysql-port INTEGER``: MySQL port. Defaults to 3306.
47+
- ``--mysql-charset TEXT``: MySQL database and table character set. The default is utf8mb4.
48+
- ``--mysql-collation TEXT``: MySQL database and table collation
4749
- ``-S, --skip-ssl``: Disable MySQL connection encryption.
4850

4951
Other Options

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ classifiers = [
3939
]
4040
dependencies = [
4141
"Click>=8.1.3",
42-
"mysql-connector-python==8.4.0",
42+
"mysql-connector-python>=9.0.0",
4343
"pytimeparse2",
4444
"python-dateutil>=2.9.0.post0",
4545
"types_python_dateutil",

requirements_dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Click>=8.1.3
22
docker>=6.1.3
33
factory-boy
44
Faker>=18.10.0
5-
mysql-connector-python>=8.3.0
5+
mysql-connector-python>=9.0.0
66
mysqlclient>=2.1.1
77
pytest>=7.3.1
88
pytest-cov

src/mysql_to_sqlite3/cli.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
from datetime import datetime
77

88
import click
9+
from mysql.connector import CharacterSet
910
from tabulate import tabulate
1011

1112
from . import MySQLtoSQLite
1213
from . import __version__ as package_version
1314
from .click_utils import OptionEatAll, prompt_password, validate_positive_integer
1415
from .debug_info import info
16+
from .mysql_utils import mysql_supported_character_sets
1517
from .sqlite_utils import CollatingSequences
1618

1719

@@ -106,6 +108,24 @@
106108
)
107109
@click.option("-h", "--mysql-host", default="localhost", help="MySQL host. Defaults to localhost.")
108110
@click.option("-P", "--mysql-port", type=int, default=3306, help="MySQL port. Defaults to 3306.")
111+
@click.option(
112+
"--mysql-charset",
113+
metavar="TEXT",
114+
type=click.Choice(list(CharacterSet().get_supported()), case_sensitive=False),
115+
default="utf8mb4",
116+
show_default=True,
117+
help="MySQL database and table character set",
118+
)
119+
@click.option(
120+
"--mysql-collation",
121+
metavar="TEXT",
122+
type=click.Choice(
123+
[charset.collation for charset in mysql_supported_character_sets()],
124+
case_sensitive=False,
125+
),
126+
default=None,
127+
help="MySQL database and table collation",
128+
)
109129
@click.option("-S", "--skip-ssl", is_flag=True, help="Disable MySQL connection encryption.")
110130
@click.option(
111131
"-c",
@@ -149,6 +169,8 @@ def cli(
149169
without_data: bool,
150170
mysql_host: str,
151171
mysql_port: int,
172+
mysql_charset: str,
173+
mysql_collation: str,
152174
skip_ssl: bool,
153175
chunk: int,
154176
log_file: t.Union[str, "os.PathLike[t.Any]"],
@@ -161,6 +183,16 @@ def cli(
161183
"""Transfer MySQL to SQLite using the provided CLI options."""
162184
click.echo(_copyright_header)
163185
try:
186+
if mysql_collation:
187+
charset_collations: t.Tuple[str, ...] = tuple(
188+
cs.collation for cs in mysql_supported_character_sets(mysql_charset.lower())
189+
)
190+
if mysql_collation not in set(charset_collations):
191+
raise click.ClickException(
192+
f"Error: Invalid value for '--collation' of charset '{mysql_charset}': '{mysql_collation}' "
193+
f"""is not one of {"'" + "', '".join(charset_collations) + "'"}."""
194+
)
195+
164196
# check if both mysql_skip_create_table and mysql_skip_transfer_data are True
165197
if without_tables and without_data:
166198
raise click.ClickException(
@@ -185,6 +217,8 @@ def cli(
185217
without_data=without_data,
186218
mysql_host=mysql_host,
187219
mysql_port=mysql_port,
220+
mysql_charset=mysql_charset,
221+
mysql_collation=mysql_collation,
188222
mysql_ssl_disabled=skip_ssl,
189223
chunk=chunk,
190224
json_as_text=json_as_text,

src/mysql_to_sqlite3/mysql_utils.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,40 @@
22

33
import typing as t
44

5+
from mysql.connector import CharacterSet
56
from mysql.connector.charsets import MYSQL_CHARACTER_SETS
67

78

89
CHARSET_INTRODUCERS: t.Tuple[str, ...] = tuple(
910
f"_{charset[0]}" for charset in MYSQL_CHARACTER_SETS if charset is not None
1011
)
12+
13+
14+
class CharSet(t.NamedTuple):
15+
"""MySQL character set as a named tuple."""
16+
17+
id: int
18+
charset: str
19+
collation: str
20+
21+
22+
def mysql_supported_character_sets(charset: t.Optional[str] = None) -> t.Iterator[CharSet]:
23+
"""Get supported MySQL character sets."""
24+
index: int
25+
info: t.Optional[t.Tuple[str, str, bool]]
26+
if charset is not None:
27+
for index, info in enumerate(MYSQL_CHARACTER_SETS):
28+
if info is not None:
29+
try:
30+
if info[0] == charset:
31+
yield CharSet(index, charset, info[1])
32+
except KeyError:
33+
continue
34+
else:
35+
for charset in CharacterSet().get_supported():
36+
for index, info in enumerate(MYSQL_CHARACTER_SETS):
37+
if info is not None:
38+
try:
39+
yield CharSet(index, charset, info[1])
40+
except KeyError:
41+
continue

src/mysql_to_sqlite3/transporter.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import mysql.connector
1515
import typing_extensions as tx
16-
from mysql.connector import errorcode
16+
from mysql.connector import CharacterSet, errorcode
1717
from mysql.connector.abstracts import MySQLConnectionAbstract
1818
from mysql.connector.types import RowItemType
1919
from tqdm import tqdm, trange
@@ -61,6 +61,14 @@ def __init__(self, **kwargs: tx.Unpack[MySQLtoSQLiteParams]) -> None:
6161

6262
self._mysql_port = kwargs.get("mysql_port", 3306) or 3306
6363

64+
self._mysql_charset = kwargs.get("mysql_charset", "utf8mb4") or "utf8mb4"
65+
66+
self._mysql_collation = (
67+
kwargs.get("mysql_collation") or CharacterSet().get_default_collation(self._mysql_charset.lower())[0]
68+
)
69+
if not kwargs.get("mysql_collation") and self._mysql_collation == "utf8mb4_0900_ai_ci":
70+
self._mysql_collation = "utf8mb4_unicode_ci"
71+
6472
self._mysql_tables = kwargs.get("mysql_tables") or tuple()
6573

6674
self._exclude_mysql_tables = kwargs.get("exclude_mysql_tables") or tuple()
@@ -128,6 +136,8 @@ def __init__(self, **kwargs: tx.Unpack[MySQLtoSQLiteParams]) -> None:
128136
host=self._mysql_host,
129137
port=self._mysql_port,
130138
ssl_disabled=self._mysql_ssl_disabled,
139+
charset=self._mysql_charset,
140+
collation=self._mysql_collation,
131141
)
132142
if isinstance(_mysql_connection, MySQLConnectionAbstract):
133143
self._mysql = _mysql_connection

src/mysql_to_sqlite3/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class MySQLtoSQLiteParams(tx.TypedDict):
2424
mysql_host: str
2525
mysql_password: t.Optional[t.Union[str, bool]]
2626
mysql_port: int
27+
mysql_charset: t.Optional[str]
28+
mysql_collation: t.Optional[str]
2729
mysql_ssl_disabled: t.Optional[bool]
2830
mysql_tables: t.Optional[t.Sequence[str]]
2931
mysql_user: str
@@ -55,6 +57,8 @@ class MySQLtoSQLiteAttributes:
5557
_mysql_host: str
5658
_mysql_password: t.Optional[str]
5759
_mysql_port: int
60+
_mysql_charset: str
61+
_mysql_collation: str
5862
_mysql_ssl_disabled: bool
5963
_mysql_tables: t.Sequence[str]
6064
_mysql_user: str

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ def mysql_instance(mysql_credentials: MySQLCredentials, pytestconfig: Config) ->
247247
password=mysql_credentials.password,
248248
host=mysql_credentials.host,
249249
port=mysql_credentials.port,
250+
charset="utf8mb4",
251+
collation="utf8mb4_unicode_ci",
250252
)
251253
except mysql.connector.Error as err:
252254
if err.errno == errorcode.CR_SERVER_LOST:

0 commit comments

Comments
 (0)