Skip to content

Commit a5413ba

Browse files
committed
✨ add support for UNSIGNED numeric data type conversion
1 parent bfe9ead commit a5413ba

File tree

2 files changed

+128
-17
lines changed

2 files changed

+128
-17
lines changed

sqlite3_to_mysql/transporter.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
convert_timedelta,
3030
unicase_compare,
3131
)
32-
3332
from .mysql_utils import (
3433
MYSQL_BLOB_COLUMN_TYPES,
3534
MYSQL_COLUMN_TYPES,
@@ -48,7 +47,7 @@ class SQLite3toMySQL(SQLite3toMySQLAttributes):
4847
"""Use this class to transfer an SQLite 3 database to MySQL."""
4948

5049
COLUMN_PATTERN: t.Pattern[str] = re.compile(r"^[^(]+")
51-
COLUMN_LENGTH_PATTERN: t.Pattern[str] = re.compile(r"\(\d+\)$")
50+
COLUMN_LENGTH_PATTERN: t.Pattern[str] = re.compile(r"\(\d+\)")
5251

5352
MYSQL_CONNECTOR_VERSION: version.Version = version.parse(mysql_connector_version_string)
5453

@@ -249,11 +248,13 @@ def _valid_column_type(cls, column_type: str) -> t.Optional[t.Match[str]]:
249248
def _translate_type_from_sqlite_to_mysql(self, column_type: str) -> str:
250249
"""This could be optimized even further, however is seems adequate."""
251250
full_column_type: str = column_type.upper()
251+
unsigned: bool = "UNSIGNED" in full_column_type
252252
match: t.Optional[t.Match[str]] = self._valid_column_type(column_type)
253253
if not match:
254254
raise ValueError(f'"{column_type}" is not a valid column_type!')
255255

256256
data_type: str = match.group(0).upper()
257+
257258
if data_type in {"TEXT", "CLOB", "STRING"}:
258259
return self._mysql_text_type
259260
if data_type in {"CHARACTER", "NCHAR", "NATIVE CHARACTER"}:
@@ -267,25 +268,36 @@ def _translate_type_from_sqlite_to_mysql(self, column_type: str) -> str:
267268
match = self._valid_column_type(self._mysql_string_type)
268269
if match:
269270
return match.group(0).upper() + length
270-
if data_type == "DOUBLE PRECISION":
271-
return "DOUBLE"
272271
if data_type == "UNSIGNED BIG INT":
273-
return "BIGINT" + self._column_type_length(column_type) + " UNSIGNED"
274-
if data_type in {"INT1", "INT2"}:
275-
return self._mysql_integer_type
276-
if data_type in {"INTEGER", "INT"}:
272+
return f"BIGINT{self._column_type_length(column_type)} UNSIGNED"
273+
if data_type.startswith(("TINYINT", "INT1")):
274+
print("length", self._column_type_length(column_type))
275+
return f"TINYINT{self._column_type_length(column_type)}{' UNSIGNED' if unsigned else ''}"
276+
if data_type.startswith(("SMALLINT", "INT2")):
277+
return f"SMALLINT{self._column_type_length(column_type)}{' UNSIGNED' if unsigned else ''}"
278+
if data_type.startswith(("MEDIUMINT", "INT3")):
279+
return f"MEDIUMINT{self._column_type_length(column_type)}{' UNSIGNED' if unsigned else ''}"
280+
if data_type.startswith("INT4"):
281+
return f"INT{self._column_type_length(column_type)}{' UNSIGNED' if unsigned else ''}"
282+
if data_type.startswith(("BIGINT", "INT8")):
283+
return f"BIGINT{self._column_type_length(column_type)}{' UNSIGNED' if unsigned else ''}"
284+
if data_type.startswith(("INT64", "NUMERIC")):
285+
return f"BIGINT{self._column_type_length(column_type, 19)}{' UNSIGNED' if unsigned else ''}"
286+
if data_type.startswith(("INTEGER", "INT")):
277287
length = self._column_type_length(column_type)
278288
if not length:
279-
return self._mysql_integer_type
289+
if "UNSIGNED" in self._mysql_integer_type:
290+
return self._mysql_integer_type
291+
return f"{self._mysql_integer_type}{' UNSIGNED' if unsigned else ''}"
280292
match = self._valid_column_type(self._mysql_integer_type)
281293
if match:
282-
if self._mysql_integer_type.endswith("UNSIGNED"):
283-
return match.group(0).upper() + length + " UNSIGNED"
284-
return match.group(0).upper() + length
285-
if data_type in {"INT64", "NUMERIC"}:
286-
return "BIGINT" + self._column_type_length(column_type, 19)
294+
if "UNSIGNED" in self._mysql_integer_type:
295+
return f"{match.group(0).upper()}{length} UNSIGNED"
296+
return f"{match.group(0).upper()}{length}{' UNSIGNED' if unsigned else ''}"
287297
if data_type == "BOOLEAN":
288298
return "TINYINT(1)"
299+
if data_type.startswith(("REAL", "DOUBLE", "FLOAT", "DECIMAL", "DEC", "FIXED")):
300+
return full_column_type
289301
if data_type not in MYSQL_COLUMN_TYPES:
290302
return self._mysql_string_type
291303
return full_column_type

tests/unit/sqlite3_to_mysql_test.py

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,15 @@ def test_translate_type_from_sqlite_to_mysql_all_valid_columns(
103103
assert proc._translate_type_from_sqlite_to_mysql(f"VARCHAR({length})") == re.sub(
104104
r"\d+", str(length), proc._mysql_string_type
105105
)
106-
assert proc._translate_type_from_sqlite_to_mysql("DOUBLE PRECISION") == "DOUBLE"
106+
assert proc._translate_type_from_sqlite_to_mysql("DOUBLE PRECISION") == "DOUBLE PRECISION"
107107
assert proc._translate_type_from_sqlite_to_mysql("UNSIGNED BIG INT") == "BIGINT UNSIGNED"
108108
length = faker.pyint(min_value=1000000000, max_value=99999999999999999999)
109109
assert proc._translate_type_from_sqlite_to_mysql(f"UNSIGNED BIG INT({length})") == f"BIGINT({length}) UNSIGNED"
110-
assert proc._translate_type_from_sqlite_to_mysql("INT1") == proc._mysql_integer_type
111-
assert proc._translate_type_from_sqlite_to_mysql("INT2") == proc._mysql_integer_type
110+
assert proc._translate_type_from_sqlite_to_mysql("INT1") == "TINYINT"
111+
assert proc._translate_type_from_sqlite_to_mysql("INT2") == "SMALLINT"
112+
assert proc._translate_type_from_sqlite_to_mysql("INT3") == "MEDIUMINT"
113+
assert proc._translate_type_from_sqlite_to_mysql("INT4") == "INT"
114+
assert proc._translate_type_from_sqlite_to_mysql("INT8") == "BIGINT"
112115
length = faker.pyint(min_value=1, max_value=11)
113116
assert proc._translate_type_from_sqlite_to_mysql(f"INT({length})") == re.sub(
114117
r"\d+", str(length), proc._mysql_integer_type
@@ -122,6 +125,102 @@ def test_translate_type_from_sqlite_to_mysql_all_valid_columns(
122125
== f"DECIMAL({precision},{scale})"
123126
)
124127

128+
@pytest.mark.parametrize(
129+
"sqlite_data_type, mysql_data_type",
130+
[
131+
("INT", "INT(11)"),
132+
("INT(5)", "INT(5)"),
133+
("INT UNSIGNED", "INT(11) UNSIGNED"),
134+
("INT(5) UNSIGNED", "INT(5) UNSIGNED"),
135+
("INTEGER", "INT(11)"),
136+
("TINYINT", "TINYINT"),
137+
("TINYINT UNSIGNED", "TINYINT UNSIGNED"),
138+
("TINYINT(4)", "TINYINT(4)"),
139+
("TINYINT(4) UNSIGNED", "TINYINT(4) UNSIGNED"),
140+
("SMALLINT", "SMALLINT"),
141+
("SMALLINT UNSIGNED", "SMALLINT UNSIGNED"),
142+
("SMALLINT(6)", "SMALLINT(6)"),
143+
("SMALLINT(6) UNSIGNED", "SMALLINT(6) UNSIGNED"),
144+
("MEDIUMINT", "MEDIUMINT"),
145+
("MEDIUMINT UNSIGNED", "MEDIUMINT UNSIGNED"),
146+
("MEDIUMINT(9)", "MEDIUMINT(9)"),
147+
("MEDIUMINT(9) UNSIGNED", "MEDIUMINT(9) UNSIGNED"),
148+
("BIGINT", "BIGINT"),
149+
("BIGINT UNSIGNED", "BIGINT UNSIGNED"),
150+
("BIGINT(20)", "BIGINT(20)"),
151+
("BIGINT(20) UNSIGNED", "BIGINT(20) UNSIGNED"),
152+
("UNSIGNED BIG INT", "BIGINT UNSIGNED"),
153+
("INT1", "TINYINT"),
154+
("INT1 UNSIGNED", "TINYINT UNSIGNED"),
155+
("INT1(3)", "TINYINT(3)"),
156+
("INT1(3) UNSIGNED", "TINYINT(3) UNSIGNED"),
157+
("INT2", "SMALLINT"),
158+
("INT2 UNSIGNED", "SMALLINT UNSIGNED"),
159+
("INT2(6)", "SMALLINT(6)"),
160+
("INT2(6) UNSIGNED", "SMALLINT(6) UNSIGNED"),
161+
("INT3", "MEDIUMINT"),
162+
("INT3 UNSIGNED", "MEDIUMINT UNSIGNED"),
163+
("INT3(9)", "MEDIUMINT(9)"),
164+
("INT3(9) UNSIGNED", "MEDIUMINT(9) UNSIGNED"),
165+
("INT4", "INT"),
166+
("INT4 UNSIGNED", "INT UNSIGNED"),
167+
("INT4(11)", "INT(11)"),
168+
("INT4(11) UNSIGNED", "INT(11) UNSIGNED"),
169+
("INT8", "BIGINT"),
170+
("INT8 UNSIGNED", "BIGINT UNSIGNED"),
171+
("INT8(19)", "BIGINT(19)"),
172+
("INT8(19) UNSIGNED", "BIGINT(19) UNSIGNED"),
173+
("NUMERIC", "BIGINT(19)"),
174+
("DOUBLE", "DOUBLE"),
175+
("DOUBLE UNSIGNED", "DOUBLE UNSIGNED"),
176+
("DOUBLE(10,5)", "DOUBLE(10,5)"),
177+
("DOUBLE(10,5) UNSIGNED", "DOUBLE(10,5) UNSIGNED"),
178+
("DOUBLE PRECISION", "DOUBLE PRECISION"),
179+
("DOUBLE PRECISION UNSIGNED", "DOUBLE PRECISION UNSIGNED"),
180+
("DOUBLE PRECISION(10,5)", "DOUBLE PRECISION(10,5)"),
181+
("DOUBLE PRECISION(10,5) UNSIGNED", "DOUBLE PRECISION(10,5) UNSIGNED"),
182+
("DECIMAL", "DECIMAL"),
183+
("DECIMAL UNSIGNED", "DECIMAL UNSIGNED"),
184+
("DECIMAL(10,5)", "DECIMAL(10,5)"),
185+
("DECIMAL(10,5) UNSIGNED", "DECIMAL(10,5) UNSIGNED"),
186+
("REAL", "REAL"),
187+
("REAL UNSIGNED", "REAL UNSIGNED"),
188+
("REAL(10,5)", "REAL(10,5)"),
189+
("REAL(10,5) UNSIGNED", "REAL(10,5) UNSIGNED"),
190+
("FLOAT", "FLOAT"),
191+
("FLOAT UNSIGNED", "FLOAT UNSIGNED"),
192+
("FLOAT(10,5)", "FLOAT(10,5)"),
193+
("FLOAT(10,5) UNSIGNED", "FLOAT(10,5) UNSIGNED"),
194+
("DEC", "DEC"),
195+
("DEC UNSIGNED", "DEC UNSIGNED"),
196+
("DEC(10,5)", "DEC(10,5)"),
197+
("DEC(10,5) UNSIGNED", "DEC(10,5) UNSIGNED"),
198+
("FIXED", "FIXED"),
199+
("FIXED UNSIGNED", "FIXED UNSIGNED"),
200+
("FIXED(10,5)", "FIXED(10,5)"),
201+
("FIXED(10,5) UNSIGNED", "FIXED(10,5) UNSIGNED"),
202+
("BOOLEAN", "TINYINT(1)"),
203+
("INT64", "BIGINT(19)"),
204+
],
205+
)
206+
def test_translate_type_from_sqlite_to_mysql_all_valid_numeric_columns_signed_unsigned(
207+
self,
208+
sqlite_database: str,
209+
mysql_database: Engine,
210+
mysql_credentials: MySQLCredentials,
211+
sqlite_data_type: str,
212+
mysql_data_type: str,
213+
) -> None:
214+
proc: SQLite3toMySQL = SQLite3toMySQL( # type: ignore
215+
sqlite_file=sqlite_database,
216+
mysql_user=mysql_credentials.user,
217+
mysql_password=mysql_credentials.password,
218+
mysql_host=mysql_credentials.host,
219+
mysql_port=mysql_credentials.port,
220+
mysql_database=mysql_credentials.database,
221+
)
222+
assert proc._translate_type_from_sqlite_to_mysql(sqlite_data_type) == mysql_data_type
223+
125224
@pytest.mark.parametrize("quiet", [False, True])
126225
def test_create_database_connection_error(
127226
self,

0 commit comments

Comments
 (0)