|
24 | 24 | from tests.conftest import MySQLCredentials |
25 | 25 |
|
26 | 26 |
|
| 27 | +SQLITE_SUPPORTS_JSONB: bool = sqlite3.sqlite_version_info >= (3, 45, 0) |
| 28 | + |
| 29 | + |
27 | 30 | def test_cli_sqlite_views_flag_propagates( |
28 | 31 | cli_runner: CliRunner, |
29 | 32 | sqlite_database: str, |
@@ -398,9 +401,34 @@ def _make_transfer_stub(mocker: MockFixture) -> SQLite3toMySQL: |
398 | 401 | instance._translate_sqlite_view_definition = mocker.MagicMock(return_value="CREATE VIEW translated AS SELECT 1") |
399 | 402 | instance._sqlite_cur.fetchall.return_value = [] |
400 | 403 | 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 | + ) |
401 | 409 | return instance |
402 | 410 |
|
403 | 411 |
|
| 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 | + |
404 | 432 | def test_transfer_creates_mysql_views(mocker: MockFixture) -> None: |
405 | 433 | instance = _make_transfer_stub(mocker) |
406 | 434 |
|
@@ -472,7 +500,85 @@ def execute_side_effect(sql, *params): |
472 | 500 |
|
473 | 501 | executed_sqls = [call.args[0] for call in instance._sqlite_cur.execute.call_args_list] |
474 | 502 | 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 |
476 | 582 |
|
477 | 583 |
|
478 | 584 | def test_translate_sqlite_view_definition_strftime_weekday() -> None: |
@@ -554,6 +660,16 @@ def test_transfer_table_data_with_chunking(mocker: MockFixture) -> None: |
554 | 660 | instance._mysql.commit.assert_called_once() |
555 | 661 |
|
556 | 662 |
|
| 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 | + |
557 | 673 | @pytest.mark.usefixtures("sqlite_database", "mysql_instance") |
558 | 674 | class TestSQLite3toMySQL: |
559 | 675 | @pytest.mark.parametrize("quiet", [False, True]) |
|
0 commit comments