Skip to content

Commit 435af97

Browse files
authored
✨ add --without-tables option (#78)
1 parent 9af779f commit 435af97

File tree

7 files changed

+140
-25
lines changed

7 files changed

+140
-25
lines changed

README.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ mysql2sqlite --help
3131
```
3232
Usage: mysql2sqlite [OPTIONS]
3333
34-
Transfer MySQL to SQLite using the provided CLI options.
34+
mysql2sqlite version 2.1.12 Copyright (c) 2019-2024 Klemen Tusar
3535
3636
Options:
3737
-f, --sqlite-file PATH SQLite3 database file [required]
@@ -44,27 +44,23 @@ Options:
4444
foreign-keys which inhibits the transfer of
4545
foreign keys. Can not be used together with
4646
--exclude-mysql-tables.
47-
4847
-e, --exclude-mysql-tables TUPLE
4948
Transfer all tables except these specific
5049
tables (space separated table names).
5150
Implies --without-foreign-keys which
5251
inhibits the transfer of foreign keys. Can
5352
not be used together with --mysql-tables.
54-
5553
-L, --limit-rows INTEGER Transfer only a limited number of rows from
5654
each table.
57-
5855
-C, --collation [BINARY|NOCASE|RTRIM]
5956
Create datatypes of TEXT affinity using a
6057
specified collation sequence. [default:
6158
BINARY]
62-
6359
-K, --prefix-indices Prefix indices with their corresponding
6460
tables. This ensures that their names remain
6561
unique across the SQLite database.
66-
6762
-X, --without-foreign-keys Do not transfer foreign keys.
63+
-Z, --without-tables Do not transfer tables, data only.
6864
-W, --without-data Do not transfer table data, DDL only.
6965
-h, --mysql-host TEXT MySQL host. Defaults to localhost.
7066
-P, --mysql-port INTEGER MySQL port. Defaults to 3306.
@@ -75,13 +71,11 @@ Options:
7571
-V, --vacuum Use the VACUUM command to rebuild the SQLite
7672
database file, repacking it into a minimal
7773
amount of disk space
78-
7974
--use-buffered-cursors Use MySQLCursorBuffered for reading the
8075
MySQL database. This can be useful in
8176
situations where multiple queries, with
8277
small result sets, need to be combined or
8378
computed with each other.
84-
8579
-q, --quiet Quiet. Display only errors.
8680
--debug Debug mode. Will throw exceptions.
8781
--version Show the version and exit.

docs/README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Transfer Options
3636
- ``-C, --collation [BINARY|NOCASE|RTRIM]``: Create datatypes of TEXT affinity using a specified collation sequence. The default is BINARY.
3737
- ``-K, --prefix-indices``: Prefix indices with their corresponding tables. This ensures that their names remain unique across the SQLite database.
3838
- ``-X, --without-foreign-keys``: Do not transfer foreign keys.
39+
- ``-Z, --without-tables``: Do not transfer tables, data only.
3940
- ``-W, --without-data``: Do not transfer table data, DDL only.
4041

4142
Connection Options

src/mysql_to_sqlite3/cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@
9292
"This ensures that their names remain unique across the SQLite database.",
9393
)
9494
@click.option("-X", "--without-foreign-keys", is_flag=True, help="Do not transfer foreign keys.")
95+
@click.option(
96+
"-Z",
97+
"--without-tables",
98+
is_flag=True,
99+
help="Do not transfer tables, data only.",
100+
)
95101
@click.option(
96102
"-W",
97103
"--without-data",
@@ -139,6 +145,7 @@ def cli(
139145
collation: t.Optional[str],
140146
prefix_indices: bool,
141147
without_foreign_keys: bool,
148+
without_tables: bool,
142149
without_data: bool,
143150
mysql_host: str,
144151
mysql_port: int,
@@ -154,6 +161,12 @@ def cli(
154161
"""Transfer MySQL to SQLite using the provided CLI options."""
155162
click.echo(_copyright_header)
156163
try:
164+
# check if both mysql_skip_create_table and mysql_skip_transfer_data are True
165+
if without_tables and without_data:
166+
raise click.ClickException(
167+
"Error: Both -Z/--without-tables and -W/--without-data are set. There is nothing to do. Exiting..."
168+
)
169+
157170
if mysql_tables and exclude_mysql_tables:
158171
raise click.UsageError("Illegal usage: --mysql-tables and --exclude-mysql-tables are mutually exclusive!")
159172

@@ -168,6 +181,7 @@ def cli(
168181
collation=collation,
169182
prefix_indices=prefix_indices,
170183
without_foreign_keys=without_foreign_keys or (mysql_tables is not None and len(mysql_tables) > 0),
184+
without_tables=without_tables,
171185
without_data=without_data,
172186
mysql_host=mysql_host,
173187
mysql_port=mysql_port,

src/mysql_to_sqlite3/transporter.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,18 @@ def __init__(self, **kwargs: tx.Unpack[MySQLtoSQLiteParams]) -> None:
5757

5858
self._mysql_password = str(kwargs.get("mysql_password")) or None
5959

60-
self._mysql_host = kwargs.get("mysql_host") or "localhost"
60+
self._mysql_host = kwargs.get("mysql_host", "localhost") or "localhost"
6161

62-
self._mysql_port = kwargs.get("mysql_port") or 3306
62+
self._mysql_port = kwargs.get("mysql_port", 3306) or 3306
6363

6464
self._mysql_tables = kwargs.get("mysql_tables") or tuple()
6565

6666
self._exclude_mysql_tables = kwargs.get("exclude_mysql_tables") or tuple()
6767

68-
if len(self._mysql_tables) > 0 and len(self._exclude_mysql_tables) > 0:
68+
if bool(self._mysql_tables) and bool(self._exclude_mysql_tables):
6969
raise ValueError("mysql_tables and exclude_mysql_tables are mutually exclusive")
7070

71-
self._limit_rows = kwargs.get("limit_rows") or 0
71+
self._limit_rows = kwargs.get("limit_rows", 0) or 0
7272

7373
if kwargs.get("collation") is not None and str(kwargs.get("collation")).upper() in {
7474
CollatingSequences.BINARY,
@@ -79,26 +79,30 @@ def __init__(self, **kwargs: tx.Unpack[MySQLtoSQLiteParams]) -> None:
7979
else:
8080
self._collation = CollatingSequences.BINARY
8181

82-
self._prefix_indices = kwargs.get("prefix_indices") or False
82+
self._prefix_indices = kwargs.get("prefix_indices", False) or False
8383

84-
if len(self._mysql_tables) > 0 or len(self._exclude_mysql_tables) > 0:
84+
if bool(self._mysql_tables) or bool(self._exclude_mysql_tables):
8585
self._without_foreign_keys = True
8686
else:
87-
self._without_foreign_keys = kwargs.get("without_foreign_keys") or False
87+
self._without_foreign_keys = bool(kwargs.get("without_foreign_keys", False))
88+
89+
self._without_data = bool(kwargs.get("without_data", False))
90+
self._without_tables = bool(kwargs.get("without_tables", False))
8891

89-
self._without_data = kwargs.get("without_data") or False
92+
if self._without_tables and self._without_data:
93+
raise ValueError("Unable to continue without transferring data or creating tables!")
9094

91-
self._mysql_ssl_disabled = kwargs.get("mysql_ssl_disabled") or False
95+
self._mysql_ssl_disabled = bool(kwargs.get("mysql_ssl_disabled", False))
9296

9397
self._current_chunk_number = 0
9498

9599
self._chunk_size = kwargs.get("chunk") or None
96100

97-
self._buffered = kwargs.get("buffered") or False
101+
self._buffered = bool(kwargs.get("buffered", False))
98102

99-
self._vacuum = kwargs.get("vacuum") or False
103+
self._vacuum = bool(kwargs.get("vacuum", False))
100104

101-
self._quiet = kwargs.get("quiet") or False
105+
self._quiet = bool(kwargs.get("quiet", False))
102106

103107
self._logger = self._setup_logger(log_file=kwargs.get("log_file") or None, quiet=self._quiet)
104108

@@ -113,7 +117,7 @@ def __init__(self, **kwargs: tx.Unpack[MySQLtoSQLiteParams]) -> None:
113117

114118
self._sqlite_cur = self._sqlite.cursor()
115119

116-
self._json_as_text = kwargs.get("json_as_text") or False
120+
self._json_as_text = bool(kwargs.get("json_as_text", False))
117121

118122
self._sqlite_json1_extension_enabled = not self._json_as_text and self._check_sqlite_json1_extension_enabled()
119123

@@ -490,7 +494,7 @@ def _build_create_table_sql(self, table_name: str) -> str:
490494
sql += primary
491495
sql = sql.rstrip(", ")
492496

493-
if not self._without_foreign_keys:
497+
if not self._without_tables and not self._without_foreign_keys:
494498
server_version: t.Optional[t.Tuple[int, ...]] = self._mysql.get_server_version()
495499
self._mysql_cur_dict.execute(
496500
"""
@@ -662,16 +666,18 @@ def transfer(self) -> None:
662666
table_name = table_name.decode()
663667

664668
self._logger.info(
665-
"%sTransferring table %s",
669+
"%s%sTransferring table %s",
666670
"[WITHOUT DATA] " if self._without_data else "",
671+
"[ONLY DATA] " if self._without_tables else "",
667672
table_name,
668673
)
669674

670675
# reset the chunk
671676
self._current_chunk_number = 0
672677

673-
# create the table
674-
self._create_table(table_name) # type: ignore[arg-type]
678+
if not self._without_tables:
679+
# create the table
680+
self._create_table(table_name) # type: ignore[arg-type]
675681

676682
if not self._without_data:
677683
# get the size of the data

src/mysql_to_sqlite3/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class MySQLtoSQLiteParams(tx.TypedDict):
3131
quiet: t.Optional[bool]
3232
sqlite_file: t.Union[str, "os.PathLike[t.Any]"]
3333
vacuum: t.Optional[bool]
34+
without_tables: t.Optional[bool]
3435
without_data: t.Optional[bool]
3536
without_foreign_keys: t.Optional[bool]
3637

@@ -62,6 +63,7 @@ class MySQLtoSQLiteAttributes:
6263
_sqlite: Connection
6364
_sqlite_cur: Cursor
6465
_sqlite_file: t.Union[str, "os.PathLike[t.Any]"]
66+
_without_tables: bool
6567
_sqlite_json1_extension_enabled: bool
6668
_vacuum: bool
6769
_without_data: bool

tests/func/mysql_to_sqlite3_test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,29 @@ def cursor(
199199
assert any("MySQL Database does not exist!" in message for message in caplog.messages)
200200
assert "Unknown database" in str(excinfo.value)
201201

202+
@pytest.mark.init
203+
def test_without_tables_and_without_data(
204+
self,
205+
sqlite_database: "os.PathLike[t.Any]",
206+
mysql_database: Database,
207+
mysql_credentials: MySQLCredentials,
208+
caplog: LogCaptureFixture,
209+
tmpdir: LocalPath,
210+
faker: Faker,
211+
) -> None:
212+
with pytest.raises(ValueError) as excinfo:
213+
MySQLtoSQLite( # type: ignore[call-arg]
214+
sqlite_file=sqlite_database,
215+
mysql_user=mysql_credentials.user,
216+
mysql_password=mysql_credentials.password,
217+
mysql_database=mysql_credentials.database,
218+
mysql_host=mysql_credentials.host,
219+
mysql_port=mysql_credentials.port,
220+
without_tables=True,
221+
without_data=True,
222+
)
223+
assert "Unable to continue without transferring data or creating tables!" in str(excinfo.value)
224+
202225
@pytest.mark.xfail
203226
@pytest.mark.init
204227
@pytest.mark.parametrize(

tests/func/test_cli.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,81 @@ def test_invalid_database_port(
219219
}
220220
)
221221

222+
def test_without_data(
223+
self,
224+
cli_runner: CliRunner,
225+
sqlite_database: "os.PathLike[t.Any]",
226+
mysql_database: Database,
227+
mysql_credentials: MySQLCredentials,
228+
) -> None:
229+
result: Result = cli_runner.invoke(
230+
mysql2sqlite,
231+
[
232+
"-f",
233+
str(sqlite_database),
234+
"-d",
235+
mysql_credentials.database,
236+
"-u",
237+
mysql_credentials.user,
238+
"--mysql-password",
239+
mysql_credentials.password,
240+
"-h",
241+
mysql_credentials.host,
242+
"-P",
243+
str(mysql_credentials.port),
244+
"-W",
245+
],
246+
)
247+
assert result.exit_code == 0
248+
249+
def test_without_tables(
250+
self,
251+
cli_runner: CliRunner,
252+
sqlite_database: "os.PathLike[t.Any]",
253+
mysql_database: Database,
254+
mysql_credentials: MySQLCredentials,
255+
) -> None:
256+
# First we need to create the tables in the SQLite database
257+
result1: Result = cli_runner.invoke(
258+
mysql2sqlite,
259+
[
260+
"-f",
261+
str(sqlite_database),
262+
"-d",
263+
mysql_credentials.database,
264+
"-u",
265+
mysql_credentials.user,
266+
"--mysql-password",
267+
mysql_credentials.password,
268+
"-h",
269+
mysql_credentials.host,
270+
"-P",
271+
str(mysql_credentials.port),
272+
"-W",
273+
],
274+
)
275+
assert result1.exit_code == 0
276+
277+
result2: Result = cli_runner.invoke(
278+
mysql2sqlite,
279+
[
280+
"-f",
281+
str(sqlite_database),
282+
"-d",
283+
mysql_credentials.database,
284+
"-u",
285+
mysql_credentials.user,
286+
"--mysql-password",
287+
mysql_credentials.password,
288+
"-h",
289+
mysql_credentials.host,
290+
"-P",
291+
str(mysql_credentials.port),
292+
"-Z",
293+
],
294+
)
295+
assert result2.exit_code == 0
296+
222297
@pytest.mark.parametrize(
223298
"chunk, vacuum, use_buffered_cursors, quiet",
224299
[

0 commit comments

Comments
 (0)