Skip to content
Merged
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ sqlite3mysql --help
```
Usage: sqlite3mysql [OPTIONS]

Transfer SQLite to MySQL using the provided CLI options.
sqlite3mysql version 2.1.10 Copyright (c) 2018-2024 Klemen Tusar

Options:
-f, --sqlite-file PATH SQLite3 database file [required]
Expand All @@ -52,7 +52,7 @@ Options:
-h, --mysql-host TEXT MySQL host. Defaults to localhost.
-P, --mysql-port INTEGER MySQL port. Defaults to 3306.
-S, --skip-ssl Disable MySQL connection encryption.
-i, --mysql-insert-method [UPDATE|IGNORE|DEFAULT]
-i, --mysql-insert-method [DEFAULT|IGNORE|UPDATE]
MySQL insert method. DEFAULT will throw
errors when encountering duplicate records;
UPDATE will update existing rows; IGNORE
Expand All @@ -64,7 +64,7 @@ Options:
to INT(11).
--mysql-string-type TEXT MySQL default string field type. Defaults to
VARCHAR(255).
--mysql-text-type [MEDIUMTEXT|TEXT|TINYTEXT|LONGTEXT]
--mysql-text-type [LONGTEXT|MEDIUMTEXT|TEXT|TINYTEXT]
MySQL default text field type. Defaults to
TEXT.
--mysql-charset TEXT MySQL database and table character set
Expand All @@ -75,6 +75,8 @@ Options:
not support InnoDB FULLTEXT indexes!
--with-rowid Transfer rowid columns.
-c, --chunk INTEGER Chunk reading/writing SQL records
-K, --mysql-skip-create-tables Skip creating tables in MySQL.
-J, --mysql-skip-transfer-data Skip transferring data to MySQL.
-l, --log-file PATH Log file
-q, --quiet Quiet. Display only errors.
--debug Debug mode. Will throw exceptions.
Expand Down
35 changes: 19 additions & 16 deletions docs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,40 @@ Password Options
- ``-p, --prompt-mysql-password``: Prompt for MySQL password
- ``--mysql-password TEXT``: MySQL password

Table Options
""""""""""""""
Connection Options
""""""""""""""""""

- ``-t, --sqlite-tables TUPLE``: Transfer only these specific tables (space separated table names). Implies ``--without-foreign-keys`` which inhibits the transfer of foreign keys.
- ``-X, --without-foreign-keys``: Do not transfer foreign keys.
- ``-W, --ignore-duplicate-keys``: Ignore duplicate keys. The default behavior is to create new ones with a numerical suffix, e.g. 'existing_key' -> 'existing_key_1'
- ``-E, --mysql-truncate-tables``: Truncates existing tables before inserting data.
- ``-h, --mysql-host TEXT``: MySQL host. Defaults to localhost.
- ``-P, --mysql-port INTEGER``: MySQL port. Defaults to 3306.
- ``-S, --skip-ssl``: Disable MySQL connection encryption.

Transfer Options
""""""""""""""""

- ``-t, --sqlite-tables TUPLE``: Transfer only these specific tables (space separated table names). Implies ``--without-foreign-keys`` which inhibits the transfer of foreign keys.
- ``-E, --mysql-truncate-tables``: Truncates existing tables before inserting data.
- ``-K, --mysql-skip-create-tables``: Skip creating tables in MySQL.
- ``-i, --mysql-insert-method [UPDATE|IGNORE|DEFAULT]``: MySQL insert method. DEFAULT will throw errors when encountering duplicate records; UPDATE will update existing rows; IGNORE will ignore insert errors. Defaults to IGNORE.
- ``-J, --mysql-skip-transfer-data``: Skip transferring data to MySQL.
- ``--mysql-integer-type TEXT``: MySQL default integer field type. Defaults to INT(11).
- ``--mysql-string-type TEXT``: MySQL default string field type. Defaults to VARCHAR(255).
- ``--mysql-text-type [LONGTEXT|MEDIUMTEXT|TEXT|TINYTEXT]``: MySQL default text field type. Defaults to TEXT.
- ``--mysql-charset TEXT``: MySQL database and table character set. Defaults to utf8mb4.
` ``--mysql-collation TEXT``: MySQL database and table collation
- ``-T, --use-fulltext``: Use FULLTEXT indexes on TEXT columns. Will throw an error if your MySQL version does not support InnoDB FULLTEXT indexes!
- ``-X, --without-foreign-keys``: Do not transfer foreign keys.
- ``-W, --ignore-duplicate-keys``: Ignore duplicate keys. The default behavior is to create new ones with a numerical suffix, e.g. 'existing_key' -> 'existing_key_1'
- ``--with-rowid``: Transfer rowid columns.
- ``-c, --chunk INTEGER``: Chunk reading/writing SQL records

Connection Options
""""""""""""""""""

- ``-h, --mysql-host TEXT``: MySQL host. Defaults to localhost.
- ``-P, --mysql-port INTEGER``: MySQL port. Defaults to 3306.
- ``-S, --skip-ssl``: Disable MySQL connection encryption.

Other Options
""""""""""""""
"""""""""""""

- ``-T, --use-fulltext``: Use FULLTEXT indexes on TEXT columns. Will throw an error if your MySQL version does not support InnoDB FULLTEXT indexes!
- ``-l, --log-file PATH``: Log file
- ``-q, --quiet``: Quiet. Display only errors.
- ``--debug``: Debug mode. Will throw exceptions.
- ``--version``: Show the version and exit.
- ``--help``: Show this message and exit.
- ``--help``: Show help message and exit.

Docker
^^^^^^
Expand Down
16 changes: 15 additions & 1 deletion src/sqlite3_to_mysql/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@
)
@click.option("--with-rowid", is_flag=True, help="Transfer rowid columns.")
@click.option("-c", "--chunk", type=int, default=None, help="Chunk reading/writing SQL records")
@click.option("-K", "--mysql-skip-create-tables", is_flag=True, help="Skip creating tables in MySQL.")
@click.option("-J", "--mysql-skip-transfer-data", is_flag=True, help="Skip transferring data to MySQL.")
@click.option("-l", "--log-file", type=click.Path(), help="Log file")
@click.option("-q", "--quiet", is_flag=True, help="Quiet. Display only errors.")
@click.option("--debug", is_flag=True, help="Debug mode. Will throw exceptions.")
Expand All @@ -146,6 +148,8 @@ def cli(
use_fulltext: bool,
with_rowid: bool,
chunk: int,
mysql_skip_create_tables: bool,
mysql_skip_transfer_data: bool,
log_file: t.Union[str, "os.PathLike[t.Any]"],
quiet: bool,
debug: bool,
Expand All @@ -159,9 +163,17 @@ def cli(
)
if mysql_collation not in set(charset_collations):
raise click.ClickException(
f"""Error: Invalid value for '--collation' of charset '{mysql_charset}': '{mysql_collation}' is not one of {"'" + "', '".join(charset_collations) + "'"}."""
f"Error: Invalid value for '--collation' of charset '{mysql_charset}': '{mysql_collation}' "
f"""is not one of {"'" + "', '".join(charset_collations) + "'"}."""
)

# check if both mysql_skip_create_table and mysql_skip_transfer_data are True
if mysql_skip_create_tables and mysql_skip_transfer_data:
raise click.ClickException(
"Error: Both -K/--mysql-skip-create-tables and -J/--mysql-skip-transfer-data are set. "
"There is nothing to do. Exiting..."
)

SQLite3toMySQL(
sqlite_file=sqlite_file,
sqlite_tables=sqlite_tables or tuple(),
Expand All @@ -183,6 +195,8 @@ def cli(
use_fulltext=use_fulltext,
with_rowid=with_rowid,
chunk=chunk,
mysql_create_tables=not mysql_skip_create_tables,
mysql_transfer_data=not mysql_skip_transfer_data,
log_file=log_file,
quiet=quiet,
).transfer()
Expand Down
16 changes: 13 additions & 3 deletions src/sqlite3_to_mysql/transporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ def __init__(self, **kwargs: tx.Unpack[SQLite3toMySQLParams]):
self._sqlite_version = self._get_sqlite_version()
self._sqlite_table_xinfo_support = check_sqlite_table_xinfo_support(self._sqlite_version)

self._mysql_create_tables = kwargs.get("mysql_create_tables") or True
self._mysql_transfer_data = kwargs.get("mysql_transfer_data") or True

if not self._mysql_transfer_data and not self._mysql_create_tables:
raise ValueError("Unable to continue without transferring data or creating tables!")

try:
_mysql_connection = mysql.connector.connect(
user=self._mysql_user,
Expand Down Expand Up @@ -677,15 +683,19 @@ def transfer(self) -> None:
transfer_rowid: bool = self._with_rowid and self._sqlite_table_has_rowid(table["name"])

# create the table
self._create_table(table["name"], transfer_rowid=transfer_rowid)
if self._mysql_create_tables:
self._create_table(table["name"], transfer_rowid=transfer_rowid)

# truncate the table on request
if self._mysql_truncate_tables:
self._truncate_table(table["name"])

# get the size of the data
self._sqlite_cur.execute(f'SELECT COUNT(*) AS total_records FROM "{table["name"]}"')
total_records = int(dict(self._sqlite_cur.fetchone())["total_records"])
if self._mysql_transfer_data:
self._sqlite_cur.execute(f'SELECT COUNT(*) AS total_records FROM "{table["name"]}"')
total_records = int(dict(self._sqlite_cur.fetchone())["total_records"])
else:
total_records = 0

# only continue if there is anything to transfer
if total_records > 0:
Expand Down
4 changes: 4 additions & 0 deletions src/sqlite3_to_mysql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ class SQLite3toMySQLParams(tx.TypedDict):
log_file: t.Optional[t.Union[str, "os.PathLike[t.Any]"]]
mysql_database: t.Optional[str]
mysql_integer_type: t.Optional[str]
mysql_create_tables: t.Optional[bool]
mysql_truncate_tables: t.Optional[bool]
mysql_transfer_data: t.Optional[bool]
mysql_charset: t.Optional[str]
mysql_collation: t.Optional[str]
ignore_duplicate_keys: t.Optional[bool]
Expand Down Expand Up @@ -54,7 +56,9 @@ class SQLite3toMySQLAttributes:
_log_file: t.Union[str, "os.PathLike[t.Any]"]
_mysql_database: str
_mysql_insert_method: str
_mysql_create_tables: bool
_mysql_truncate_tables: bool
_mysql_transfer_data: bool
_mysql_integer_type: str
_mysql_string_type: str
_mysql_text_type: str
Expand Down
74 changes: 74 additions & 0 deletions tests/func/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,80 @@ def test_invalid_database_port(
}
)

def test_mysql_skip_create_tables(
self,
cli_runner: CliRunner,
sqlite_database: str,
mysql_credentials: MySQLCredentials,
mysql_database: Engine,
) -> None:
result: Result = cli_runner.invoke(
sqlite3mysql,
[
"-f",
sqlite_database,
"-d",
mysql_credentials.database,
"-u",
mysql_credentials.user,
"--mysql-password",
mysql_credentials.password,
"--mysql-skip-create-tables",
],
)
assert result.exit_code == 0

def test_mysql_skip_transfer_data(
self,
cli_runner: CliRunner,
sqlite_database: str,
mysql_credentials: MySQLCredentials,
mysql_database: Engine,
) -> None:
result: Result = cli_runner.invoke(
sqlite3mysql,
[
"-f",
sqlite_database,
"-d",
mysql_credentials.database,
"-u",
mysql_credentials.user,
"--mysql-password",
mysql_credentials.password,
"--mysql-skip-transfer-data",
],
)
assert result.exit_code == 0

def test_mysql_skip_create_tables_and_transfer_data(
self,
cli_runner: CliRunner,
sqlite_database: str,
mysql_credentials: MySQLCredentials,
mysql_database: Engine,
) -> None:
result: Result = cli_runner.invoke(
sqlite3mysql,
[
"-f",
sqlite_database,
"-d",
mysql_credentials.database,
"-u",
mysql_credentials.user,
"--mysql-password",
mysql_credentials.password,
"--mysql-skip-create-tables",
"--mysql-skip-transfer-data",
],
)
assert result.exit_code > 0
assert (
"Error: Both -K/--mysql-skip-create-tables and -J/--mysql-skip-transfer-data are set. "
"There is nothing to do. Exiting..."
) in result.output

@pytest.mark.parametrize(
"mysql_integer_type,"
"mysql_string_type,"
Expand Down