Skip to content

fix(mysql): apply _quote_ident to all SQL paths (#442 follow-up)#514

Merged
yodakanohoshi merged 2 commits into
drt-hub:mainfrom
Godzilaa:fix/mysql-quote-ident-all-paths
May 19, 2026
Merged

fix(mysql): apply _quote_ident to all SQL paths (#442 follow-up)#514
yodakanohoshi merged 2 commits into
drt-hub:mainfrom
Godzilaa:fix/mysql-quote-ident-all-paths

Conversation

@Godzilaa

Copy link
Copy Markdown
Contributor

Closes #511

Summary

MySQLDestination._quote_ident already exists and correctly handles
schema-qualified table names (`mydb.scores` → `mydb`.`scores`), but
was only used in the swap path. This applies it to the remaining paths.

Changes

  • get_row_count: Replaced inline split+backtick logic with
    `self._quote_ident(config.table)`.
  • _load_replace: Replaced raw `TRUNCATE TABLE `{table}`` with
    `self._quote_ident(table)`.
  • _build_insert_sql: Replaced raw ``{table}`` with
    `MySQLDestination._quote_ident(table)`.
  • _build_upsert_sql: Same change for both the `INSERT ... ON
    DUPLICATE KEY UPDATE` and `INSERT IGNORE` paths.

Follows the pattern established in PR #498 for Postgres.

Replace raw backtick-quoted table names with _quote_ident() in
_load_replace, _build_insert_sql, _build_upsert_sql, and get_row_count
so schema-qualified table names (e.g. mydb.scores) work in all modes.

Closes drt-hub#511
Copilot AI review requested due to automatic review settings May 14, 2026 18:36
@github-actions

github-actions Bot commented May 14, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR applies MySQL table identifier quoting consistently across row-count, replace, insert, and upsert SQL generation so schema-qualified table names are rendered correctly.

Changes:

  • Reuses _quote_ident in get_row_count and replace-mode TRUNCATE.
  • Uses _quote_ident in insert and upsert SQL builders.
  • Aligns MySQL qualified identifier handling with the existing swap path.
Comments suppressed due to low confidence (3)

drt/destinations/mysql.py:366

  • These updated return expressions exceed the repository's configured 100-character Ruff line length (pyproject.toml:99), so ruff check drt tests will fail. Please wrap them before merging.
                f"INSERT INTO {MySQLDestination._quote_ident(table)} ({cols_str}) VALUES ({placeholders}) "
                f"ON DUPLICATE KEY UPDATE {set_clause}"
            )
        # All columns are part of the key — just ignore duplicates
        return f"INSERT IGNORE INTO {MySQLDestination._quote_ident(table)} ({cols_str}) VALUES ({placeholders})"

drt/destinations/mysql.py:347

  • This introduces the schema-qualified table behavior for the INSERT path, but the PR does not add MySQL regression coverage for schema-qualified insert/replace/upsert paths. Existing MySQL tests only cover unqualified insert/upsert SQL, while schema-qualified coverage exists only for get_row_count, so this could regress without detection.
        return f"INSERT INTO {MySQLDestination._quote_ident(table)} ({cols_str}) VALUES ({placeholders})"

drt/destinations/mysql.py:366

  • The qualified identifier change for the upsert SQL paths lacks MySQL regression coverage. Existing upsert tests assert unqualified table names only, so both the ON DUPLICATE KEY UPDATE and INSERT IGNORE schema-qualified cases can regress without failing tests.
                f"INSERT INTO {MySQLDestination._quote_ident(table)} ({cols_str}) VALUES ({placeholders}) "
                f"ON DUPLICATE KEY UPDATE {set_clause}"
            )
        # All columns are part of the key — just ignore duplicates
        return f"INSERT IGNORE INTO {MySQLDestination._quote_ident(table)} ({cols_str}) VALUES ({placeholders})"

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread drt/destinations/mysql.py Outdated
cols_str = ", ".join(f"`{c}`" for c in columns)
placeholders = ", ".join(["%s"] * len(columns))
return f"INSERT INTO `{table}` ({cols_str}) VALUES ({placeholders})"
return f"INSERT INTO {MySQLDestination._quote_ident(table)} ({cols_str}) VALUES ({placeholders})"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Comment thread drt/destinations/mysql.py

if not self._replace_truncated:
cur.execute(f"TRUNCATE TABLE `{table}`")
cur.execute(f"TRUNCATE TABLE {self._quote_ident(table)}")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

@masukai masukai requested a review from yodakanohoshi May 16, 2026 14:18
@yodakanohoshi

Copy link
Copy Markdown
Contributor

Welcome to drt, @Godzilaa! 🎉 Thanks for the clean MySQL-side follow-up to
#442. All four call sites listed in issue #511's AC table
(_load_replace / _build_insert_sql / _build_upsert_sql / get_row_count)
are migrated to _quote_ident exactly as prescribed — scope matches the
issue perfectly.

I also compared this against #498 (Postgres), and the divergence in
approach is the right call
. psycopg2 needs schema/relation split helpers
because of sql.Identifier(), but MySQL's string-based _quote_ident
handles mydb.scores correctly on its own — so mirroring the helper
machinery wasn't necessary. Same end result with a minimal diff. Good
judgment.

A few things to address before merge.

🚫 Blockers

1. CLA signature

The CLA Assistant bot is blocking CI. Could you post the following comment
on this PR:

I have read the CLA Document and I hereby sign the CLA

2. Missing regression tests for schema-qualified table names

Issue #511's AC requires:

Unit tests cover schema-qualified table names (e.g. mydb.scores) for
replace, upsert, and get_row_count paths — mirroring the cases added
in PR #498

This is currently unmet. Given that the substantive bug fix here is exactly
`mydb.scores``mydb`.`scores`, regression coverage matters —
at minimum 3 tests (ideally 5) would let me merge this with confidence.

PR #498's tests/unit/test_postgres_destination.py
test_*_uses_schema_qualified_identifier series is the reference template.
The MySQL test file tests/unit/test_mysql_destination.py
already uses the same mock_connect pattern, so porting should be
mechanical.

At minimum — the following 3 tests satisfy the AC:

# Add to TestUpsertSql
def test_qualified_table_on_duplicate_key(self) -> None:
    sql = MySQLDestination._build_upsert_sql(
        table="mydb.scores",
        columns=["id", "score"],
        update_cols=["score"],
    )
    assert "INSERT INTO `mydb`.`scores`" in sql
    assert "`mydb.scores`" not in sql  # regression guard for the old bug

# Add to TestMySQLReplaceMode
@patch("drt.destinations.mysql.MySQLDestination._connect")
def test_replace_uses_qualified_identifier(
    self, mock_connect: MagicMock
) -> None:
    conn = _fake_connection()
    cur = conn.cursor()
    mock_connect.return_value = conn

    MySQLDestination().load(
        [{"user_id": 1, "company_id": 5, "score": 0.5}],
        _config(table="mydb.learning_profiles"),
        _options(mode="replace"),
    )

    sqls = [c[0][0] for c in cur.execute.call_args_list]
    assert any("TRUNCATE TABLE `mydb`.`learning_profiles`" in s for s in sqls)
    assert any("INSERT INTO `mydb`.`learning_profiles`" in s for s in sqls)

# Add to TestMySQLDestinationLoad
@patch("drt.destinations.mysql.MySQLDestination._connect")
def test_get_row_count_with_qualified_table(
    self, mock_connect: MagicMock
) -> None:
    conn = _fake_connection()
    cur = conn.cursor()
    cur.fetchone.return_value = (42,)
    mock_connect.return_value = conn

    count = MySQLDestination().get_row_count(
        _config(table="mydb.learning_profiles")
    )

    assert count == 42
    sql = cur.execute.call_args.args[0]
    assert "`mydb`.`learning_profiles`" in sql
    assert "`mydb.learning_profiles`" not in sql

Optional (for better coverage) — one test each for the qualified
_build_insert_sql case and the _build_upsert_sql INSERT IGNORE branch
(update_cols=[]). That would individually cover all 4 actual bug-fix
sites in this PR.

3. Line length exceeds ruff's 100-char limit

As Copilot's review also flagged, make lint (ruff check) will fail.
Extracting the _quote_ident call into a local variable resolves all three
cleanly:

# drt/destinations/mysql.py L347 (_build_insert_sql)
table_q = MySQLDestination._quote_ident(table)
return f"INSERT INTO {table_q} ({cols_str}) VALUES ({placeholders})"

# Same pattern for L362, L366 in _build_upsert_sql — pull table_q at the top
table_q = MySQLDestination._quote_ident(table)
if update_cols:
    set_clause = ", ".join(f"`{c}` = VALUES(`{c}`)" for c in update_cols)
    return (
        f"INSERT INTO {table_q} ({cols_str}) VALUES ({placeholders}) "
        f"ON DUPLICATE KEY UPDATE {set_clause}"
    )
return f"INSERT IGNORE INTO {table_q} ({cols_str}) VALUES ({placeholders})"

A note on the @copilot apply changes comments — apologies, this one's on
our side: this repo doesn't have the Copilot coding agent enabled, so those
requests don't get auto-applied. Sorry for the confusion 🙇 Would you mind
applying the fix manually?

🔧 Nice-to-have (optional, fine to fold into the same push)

4. CHANGELOG.md entry

A one-liner under the [Unreleased]### Fixed section would be helpful:

- **MySQL destination correctly quotes schema-qualified table names**
  (#511): `mydb.scores` now produces `` `mydb`.`scores` `` across replace,
  insert, and upsert paths (was treated as a single identifier).

Next step

Once CLA signature + 3 tests + line-length fix are pushed, I'll re-check CI
and merge. The #498 test patterns port over to MySQL almost verbatim, so
the work itself should be mechanical.

Don't hesitate to drop a comment here if you get stuck or want a sanity
check — happy to work through it together 🙌 Thanks again for the
contribution!

@Godzilaa

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@Godzilaa

Godzilaa commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

recheck @yodakanohoshi

Muawiya-contact pushed a commit to Muawiya-contact/drt that referenced this pull request May 18, 2026

@masukai masukai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the textbook tight follow-up to #442 — applies the existing _quote_ident to the four remaining call sites consistently, and the refactor to introduce table_q = MySQLDestination._quote_ident(table) as a local before the f-strings is genuinely cleaner than just wrapping the long lines.

What I especially like about the new tests:

  • Each qualified-table assertion pairs a positive check (INSERT INTO \mydb`.`scores`) with a **negative check (`mydb.scores`` not in sql)**. The negative one is what would have caught the original bug, and is what locks down the fix against future regressions. Nice design.
  • Coverage now hits all four affected paths: get_row_count, _load_replace (TRUNCATE), _build_insert_sql, _build_upsert_sql (both ON DUPLICATE KEY UPDATE and INSERT IGNORE branches). No gaps left from Copilot's earlier callout.

CI all green, no further notes from me. Approving 🚀

Thanks @Godzilaa for picking this one up cleanly and turning the Copilot review around so quickly — that's a great first-contribution shape 🙏

A few engagement pointers if you'd like to stay involved:

  • If drt is useful to you, a ⭐ on the repo really helps signal traction: https://github.com/drt-hub/drt
  • The exact same shape of work is open for ClickHouse in #512 (identifier quoting audit across SQL command paths) — unassigned, and your MySQL approach here would translate directly. No pressure though.

@masukai

masukai commented May 19, 2026

Copy link
Copy Markdown
Contributor

@yodakanohoshi — when you have a minute, would you mind dropping a formal approval here? I've reviewed and approved on my side, and we'd talked through the changes already, but branch protection is (correctly) gating on your second review so the merge is held. 30-second job, no need to re-read in depth — just a green tick for the audit trail.

Thanks 🙏

@yodakanohoshi yodakanohoshi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the lag on my end — thanks @masukai for picking it up. Coverage confirmed across all four affected paths, and the table_q local read better than wrapped f-strings — exactly the cleanup direction I'd hoped for. CLA + tests + lint + CHANGELOG all in. Thanks for the quick turnaround @Godzilaa. Merging.

@yodakanohoshi yodakanohoshi enabled auto-merge (squash) May 19, 2026 03:27
@codecov

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@yodakanohoshi yodakanohoshi disabled auto-merge May 19, 2026 03:29
@yodakanohoshi yodakanohoshi merged commit 08505c6 into drt-hub:main May 19, 2026
6 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators May 19, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(mysql): apply existing _quote_ident to all SQL paths (#442 follow-up)

4 participants