Skip to content

Commit 8da02ee

Browse files
committed
✨ Transfer JSON columns as JSON (#22)
1 parent ee0a836 commit 8da02ee

File tree

5 files changed

+39
-12
lines changed

5 files changed

+39
-12
lines changed

README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,39 +38,34 @@ Options:
3838
-u, --mysql-user TEXT MySQL user [required]
3939
-p, --prompt-mysql-password Prompt for MySQL password
4040
--mysql-password TEXT MySQL password
41-
-t, --mysql-tables TEXT Transfer only these specific tables (space
41+
-t, --mysql-tables TUPLE Transfer only these specific tables (space
4242
separated table names). Implies --without-
4343
foreign-keys which inhibits the transfer of
4444
foreign keys.
45-
4645
-L, --limit-rows INTEGER Transfer only a limited number of rows from
4746
each table.
48-
4947
-C, --collation [BINARY|NOCASE|RTRIM]
5048
Create datatypes of TEXT affinity using a
5149
specified collation sequence. [default:
5250
BINARY]
53-
5451
-K, --prefix-indices Prefix indices with their corresponding
5552
tables. This ensures that their names remain
5653
unique across the SQLite database.
57-
5854
-X, --without-foreign-keys Do not transfer foreign keys.
5955
-h, --mysql-host TEXT MySQL host. Defaults to localhost.
6056
-P, --mysql-port INTEGER MySQL port. Defaults to 3306.
6157
-S, --skip-ssl Disable MySQL connection encryption.
6258
-c, --chunk INTEGER Chunk reading/writing SQL records
6359
-l, --log-file PATH Log file
60+
--json-as-text Transfer JSON columns as TEXT.
6461
-V, --vacuum Use the VACUUM command to rebuild the SQLite
6562
database file, repacking it into a minimal
6663
amount of disk space
67-
6864
--use-buffered-cursors Use MySQLCursorBuffered for reading the
6965
MySQL database. This can be useful in
7066
situations where multiple queries, with
7167
small result sets, need to be combined or
7268
computed with each other.
73-
7469
-q, --quiet Quiet. Display only errors.
7570
--version Show the version and exit.
7671
--help Show this message and exit.

mysql_to_sqlite3/cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
help="Chunk reading/writing SQL records",
9191
)
9292
@click.option("-l", "--log-file", type=click.Path(), help="Log file")
93+
@click.option("--json-as-text", is_flag=True, help="Transfer JSON columns as TEXT.")
9394
@click.option(
9495
"-V",
9596
"--vacuum",
@@ -124,6 +125,7 @@ def cli(
124125
skip_ssl,
125126
chunk,
126127
log_file,
128+
json_as_text,
127129
vacuum,
128130
use_buffered_cursors,
129131
quiet,
@@ -145,6 +147,7 @@ def cli(
145147
mysql_port=mysql_port,
146148
mysql_ssl_disabled=skip_ssl,
147149
chunk=chunk,
150+
json_as_text=json_as_text,
148151
vacuum=vacuum,
149152
buffered=use_buffered_cursors,
150153
log_file=log_file,

mysql_to_sqlite3/transporter.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ def __init__(self, **kwargs):
109109

110110
self._sqlite_cur = self._sqlite.cursor()
111111

112+
self._json_as_text = kwargs.get("json_as_text") or False
113+
self._sqlite_json1_extension_enabled = (
114+
not self._json_as_text and self._check_sqlite_json1_extension_enabled()
115+
)
116+
112117
try:
113118
self._mysql = mysql.connector.connect(
114119
user=self._mysql_user,
@@ -170,7 +175,9 @@ def _column_type_length(cls, column_type):
170175
return ""
171176

172177
@classmethod
173-
def _translate_type_from_mysql_to_sqlite(cls, column_type):
178+
def _translate_type_from_mysql_to_sqlite(
179+
cls, column_type, sqlite_json1_extension_enabled=False
180+
):
174181
"""Handle MySQL 8."""
175182
try:
176183
column_type = column_type.decode()
@@ -223,6 +230,8 @@ def _translate_type_from_mysql_to_sqlite(cls, column_type):
223230
return "INTEGER"
224231
if data_type in "TIMESTAMP":
225232
return "DATETIME"
233+
if data_type == "JSON" and sqlite_json1_extension_enabled:
234+
return "JSON"
226235
return "TEXT"
227236

228237
@classmethod
@@ -273,6 +282,13 @@ def _data_type_collation_sequence(
273282
return "COLLATE {collation}".format(collation=collation)
274283
return ""
275284

285+
def _check_sqlite_json1_extension_enabled(self):
286+
try:
287+
self._sqlite_cur.execute("PRAGMA compile_options")
288+
return "ENABLE_JSON1" in set(row[0] for row in self._sqlite_cur.fetchall())
289+
except sqlite3.Error:
290+
return False
291+
276292
def _build_create_table_sql(self, table_name):
277293
sql = 'CREATE TABLE IF NOT EXISTS "{}" ('.format(table_name)
278294
primary = ""
@@ -281,7 +297,10 @@ def _build_create_table_sql(self, table_name):
281297
self._mysql_cur_dict.execute("SHOW COLUMNS FROM `{}`".format(table_name))
282298

283299
for row in self._mysql_cur_dict.fetchall():
284-
column_type = self._translate_type_from_mysql_to_sqlite(row["Type"])
300+
column_type = self._translate_type_from_mysql_to_sqlite(
301+
column_type=row["Type"],
302+
sqlite_json1_extension_enabled=self._sqlite_json1_extension_enabled,
303+
)
285304
sql += '\n\t"{name}" {type} {notnull} {default} {collation},'.format(
286305
name=row["Field"],
287306
type=column_type,

tests/unit/mysql_to_sqlite3_test.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class TestMySQLtoSQLiteClassmethods:
1616
def test_translate_type_from_mysql_to_sqlite_invalid_column_type(self, mocker):
1717
with pytest.raises(ValueError) as excinfo:
1818
mocker.patch.object(MySQLtoSQLite, "_valid_column_type", return_value=False)
19-
MySQLtoSQLite._translate_type_from_mysql_to_sqlite("text")
19+
MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type="text")
2020
assert "Invalid column_type!" in str(excinfo.value)
2121

2222
def test_translate_type_from_mysql_to_sqlite_all_valid_columns(self):
@@ -99,7 +99,6 @@ def test_translate_type_from_mysql_to_sqlite_all_valid_columns(self):
9999
)
100100
elif column_type in {
101101
"ENUM",
102-
"JSON",
103102
"LONGTEXT",
104103
"MEDIUMTEXT",
105104
"SET",
@@ -109,6 +108,17 @@ def test_translate_type_from_mysql_to_sqlite_all_valid_columns(self):
109108
MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
110109
== "TEXT"
111110
)
111+
elif column_type == "JSON":
112+
assert (
113+
MySQLtoSQLite._translate_type_from_mysql_to_sqlite(column_type)
114+
== "TEXT"
115+
)
116+
assert (
117+
MySQLtoSQLite._translate_type_from_mysql_to_sqlite(
118+
column_type, sqlite_json1_extension_enabled=True
119+
)
120+
== "JSON"
121+
)
112122
elif column_type.endswith(" UNSIGNED"):
113123
if column_type.startswith("INT "):
114124
assert (

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ skip_install = true
5757
deps =
5858
pylint
5959
-rrequirements_dev.txt
60-
disable = C0301,C0411,R,W0107,W0622
60+
disable = C0209,C0301,C0411,R,W0107,W0622
6161
commands =
6262
pylint --rcfile=tox.ini mysql_to_sqlite3
6363

0 commit comments

Comments
 (0)