Skip to content

Commit 1698f4a

Browse files
committed
✅ add tests for JSONB column handling and conversion in SQLite to MySQL transfer
1 parent 409e7a9 commit 1698f4a

File tree

1 file changed

+117
-1
lines changed

1 file changed

+117
-1
lines changed

tests/unit/sqlite3_to_mysql_test.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
from tests.conftest import MySQLCredentials
2525

2626

27+
SQLITE_SUPPORTS_JSONB: bool = sqlite3.sqlite_version_info >= (3, 45, 0)
28+
29+
2730
def test_cli_sqlite_views_flag_propagates(
2831
cli_runner: CliRunner,
2932
sqlite_database: str,
@@ -398,9 +401,34 @@ def _make_transfer_stub(mocker: MockFixture) -> SQLite3toMySQL:
398401
instance._translate_sqlite_view_definition = mocker.MagicMock(return_value="CREATE VIEW translated AS SELECT 1")
399402
instance._sqlite_cur.fetchall.return_value = []
400403
instance._sqlite_cur.execute.return_value = None
404+
instance._get_table_info = mocker.MagicMock(
405+
return_value=[
406+
{"name": "c1", "type": "TEXT", "hidden": 0},
407+
]
408+
)
401409
return instance
402410

403411

412+
class RecordingMySQLCursor:
413+
def __init__(self) -> None:
414+
self.executed_sql: t.List[str] = []
415+
self.inserted_batches: t.List[t.List[t.Tuple[t.Any, ...]]] = []
416+
417+
def execute(self, sql: str, params: t.Optional[t.Tuple[t.Any, ...]] = None) -> None:
418+
del params
419+
self.executed_sql.append(sql)
420+
421+
def fetchall(self) -> t.List[t.Any]:
422+
return []
423+
424+
def fetchone(self) -> t.Optional[t.Any]:
425+
return None
426+
427+
def executemany(self, sql: str, rows: t.Iterable[t.Tuple[t.Any, ...]]) -> None:
428+
self.executed_sql.append(sql)
429+
self.inserted_batches.append([tuple(row) for row in rows])
430+
431+
404432
def test_transfer_creates_mysql_views(mocker: MockFixture) -> None:
405433
instance = _make_transfer_stub(mocker)
406434

@@ -472,7 +500,85 @@ def execute_side_effect(sql, *params):
472500

473501
executed_sqls = [call.args[0] for call in instance._sqlite_cur.execute.call_args_list]
474502
assert 'SELECT COUNT(*) AS total_records FROM "tbl""quote"' in executed_sqls
475-
assert 'SELECT * FROM "tbl""quote"' in executed_sqls
503+
assert 'SELECT "c1" FROM "tbl""quote"' in executed_sqls
504+
505+
506+
def test_transfer_selects_jsonb_columns_via_json_function(mocker: MockFixture) -> None:
507+
instance = _make_transfer_stub(mocker)
508+
instance._mysql_transfer_data = True
509+
instance._sqlite_cur.fetchone.return_value = {"total_records": 1}
510+
instance._sqlite_cur.fetchall.return_value = [(1, b"blob")]
511+
instance._get_table_info.return_value = [
512+
{"name": "id", "type": "INTEGER", "hidden": 0},
513+
{"name": "payload", "type": "JSONB", "hidden": 0},
514+
]
515+
516+
def execute_side_effect(sql, *params):
517+
del params
518+
if 'json("payload")' in sql:
519+
instance._sqlite_cur.description = [("id",), ("payload",)]
520+
return None
521+
522+
instance._sqlite_cur.execute.side_effect = execute_side_effect
523+
instance._fetch_sqlite_master_rows = mocker.MagicMock(side_effect=[[{"name": "tbl", "type": "table"}], []])
524+
525+
instance.transfer()
526+
527+
executed_sqls = [call.args[0] for call in instance._sqlite_cur.execute.call_args_list]
528+
json_selects = [sql for sql in executed_sqls if 'json("payload")' in sql]
529+
assert json_selects
530+
531+
532+
@pytest.mark.skipif(not SQLITE_SUPPORTS_JSONB, reason="SQLite 3.45+ required for JSONB tests")
533+
def test_transfer_converts_jsonb_values_to_textual_json(mocker: MockFixture) -> None:
534+
sqlite_connection = sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES)
535+
sqlite_connection.row_factory = sqlite3.Row
536+
sqlite_cursor = sqlite_connection.cursor()
537+
sqlite_cursor.execute("CREATE TABLE data (id INTEGER PRIMARY KEY, payload JSONB)")
538+
sqlite_cursor.execute("INSERT INTO data(payload) VALUES (jsonb(?))", ('{"foo":"bar"}',))
539+
sqlite_cursor.execute("INSERT INTO data(payload) VALUES (NULL)")
540+
sqlite_connection.commit()
541+
542+
instance = SQLite3toMySQL.__new__(SQLite3toMySQL)
543+
instance._sqlite = sqlite_connection
544+
instance._sqlite_cur = sqlite_connection.cursor()
545+
instance._sqlite_tables = tuple()
546+
instance._exclude_sqlite_tables = tuple()
547+
instance._sqlite_views_as_tables = False
548+
instance._sqlite_table_xinfo_support = True
549+
instance._mysql_create_tables = False
550+
instance._mysql_transfer_data = True
551+
instance._mysql_truncate_tables = False
552+
instance._mysql_insert_method = "IGNORE"
553+
instance._mysql_version = "8.0.32"
554+
instance._without_foreign_keys = True
555+
instance._use_fulltext = False
556+
instance._mysql_fulltext_support = False
557+
instance._with_rowid = False
558+
instance._chunk_size = None
559+
instance._quiet = True
560+
instance._mysql_charset = "utf8mb4"
561+
instance._mysql_collation = "utf8mb4_unicode_ci"
562+
instance._mysql_cur = RecordingMySQLCursor()
563+
instance._mysql = mocker.MagicMock()
564+
instance._mysql.commit = mocker.MagicMock()
565+
instance._logger = mocker.MagicMock()
566+
instance._create_table = mocker.MagicMock()
567+
instance._truncate_table = mocker.MagicMock()
568+
instance._add_indices = mocker.MagicMock()
569+
instance._add_foreign_keys = mocker.MagicMock()
570+
instance._create_mysql_view = mocker.MagicMock()
571+
instance._translate_sqlite_view_definition = mocker.MagicMock()
572+
instance._sqlite_table_has_rowid = lambda table: False
573+
instance._fetch_sqlite_master_rows = mocker.MagicMock(side_effect=[[{"name": "data", "type": "table"}], []])
574+
575+
instance.transfer()
576+
577+
assert instance._mysql_cur.inserted_batches, "expected captured MySQL inserts"
578+
inserted_rows = instance._mysql_cur.inserted_batches[0]
579+
payload_by_id = {row[0]: row[1] for row in inserted_rows}
580+
assert payload_by_id[1] == '{"foo":"bar"}'
581+
assert payload_by_id[2] is None
476582

477583

478584
def test_translate_sqlite_view_definition_strftime_weekday() -> None:
@@ -554,6 +660,16 @@ def test_transfer_table_data_with_chunking(mocker: MockFixture) -> None:
554660
instance._mysql.commit.assert_called_once()
555661

556662

663+
def test_translate_type_from_sqlite_maps_jsonb_to_json() -> None:
664+
instance = SQLite3toMySQL.__new__(SQLite3toMySQL)
665+
instance._mysql_text_type = "TEXT"
666+
instance._mysql_string_type = "VARCHAR(255)"
667+
instance._mysql_integer_type = "INT"
668+
669+
assert instance._translate_type_from_sqlite_to_mysql("JSONB") == "JSON"
670+
assert instance._translate_type_from_sqlite_to_mysql("jsonb(16)") == "JSON"
671+
672+
557673
@pytest.mark.usefixtures("sqlite_database", "mysql_instance")
558674
class TestSQLite3toMySQL:
559675
@pytest.mark.parametrize("quiet", [False, True])

0 commit comments

Comments
 (0)