Skip to content

Add Cloud SQL for MySQL → TiDB Cloud migration module - #5

Open
ayenller wants to merge 3 commits into
pingcap-inc:mainfrom
ayenller:cloudsql-to-tidb
Open

ayenller wants to merge 3 commits into
pingcap-inc:mainfrom
ayenller:cloudsql-to-tidb

Conversation

@ayenller

@ayenller ayenller commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Adds cloudsql-to-tidb, a TiShift module for migrating Google Cloud SQL for
MySQL to TiDB Cloud, alongside the existing aurora-to-tidb and
heatwave-to-tidb modules.

Why a separate module

Cloud SQL for MySQL is MySQL, so the schema-level work is nearly an identity
mapping — that alone would not justify a module. The value is entirely in the
managed-platform layer, where the standard MySQL migration advice is
actively wrong:

Fact Consequence
No user holds SUPER (not even cloudsqlsuperuser) Every SET GLOBAL fails; all variable fixes must go through gcloud
--database-flags has replace semantics Omitting an existing flag silently clears it
Binary logging is an instance setting (--enable-bin-log), not a flag "set log_bin=ON" is not actionable
binlog_format is not user-configurable Telling the user to patch it is a wrong instruction
Auth Proxy is a local client connector A scan that connects does not mean DM can
Read replicas cannot be an external replica's source Incremental-sync plans die right before cutover
lower_case_table_names is immutable after creation Case conflicts have no source-side fix

The module's rule is: when generic MySQL advice does not hold on Cloud SQL,
emit the correct alternative command, or say plainly that it cannot be
changed. A wrong fix command is worse than none — the operator runs it, then
spends time working out why nothing happened.

What's in it

  • SKILL.md — the interactive AI runbook, the primary deliverable
  • tishift_cloudsql/ — scan / convert CLI, with load, check, sync
    stubs that exit non-zero so scripts cannot mistake them for completed phases
  • references/ — the canonical rule corpus, mirrored by rules/*.py
  • docs/ — per-phase operator guides and checklist
  • tests/ — 232 tests, fully offline, no database required

Verified against a live instance

The final commit is not speculative hardening — the module was executed
end-to-end against a real Cloud SQL for MySQL 8.4.10 instance and a TiDB Cloud
Dedicated v8.5.x cluster, and every change in it is backed by an observed
failure:

  • utf8mb3 widening silently changed collation semantics. CHARSET=utf8mb3
    implies utf8mb3_general_ci (case-insensitive) on MySQL; rewritten to a bare
    CHARSET=utf8mb4 it inherits utf8mb4_bin (case-sensitive) on TiDB. That
    flips equality predicates and changes what a UNIQUE key rejects. The
    collation is now pinned explicitly whenever the source left it implicit.
  • The valid-index check was instance-wide. On a single-schema migration off
    a shared instance it counted other databases' tables: 2 in-scope tables
    reported as 6, 12 points deducted instead of 4.
  • TLS was wrong in both directions. tls: false passed no ssl argument, so
    pymysql negotiated TLS opportunistically; tls: true without a CA passed
    {"ca": ""}, which demands verification with nothing to verify against.
  • information_schema returns UPPERCASE column names regardless of the
    SELECT's casing; the collector raised KeyError against a real server.

New rule CSQL-WARNING-14 — foreign keys referencing a non-unique parent
index. A prefix of a unique key is not unique: PRIMARY KEY (branch_code, debtor_no) leaves branch_code duplicable, so a child FK on it alone is
unbacked. MySQL 8.0.16+ rejects these with ERROR 6125. Confirmed on a
250-table ERP schema — apply stops at table 57 on Cloud SQL, while TiDB
creates all 250 and enforces the FK correctly. Scored at 0: it is a dump-
portability problem (rollback targets, staging refreshes), not a TiDB blocker.

CSQL-WARNING-11 rescored to 0. Binlog retention is governed by the
instance's transactionLogRetentionDays, which is not exposed over the MySQL
protocol and does not track binlog_expire_logs_seconds — observed: an
instance raised 1 → 7 days while the variable stayed at 86400 and the setting
never appeared in databaseFlags. Penalizing a value the scanner cannot read
is wrong; the check now directs the operator to verify with gcloud.

Testing

232 passed

All offline. No credentials or live infrastructure identifiers are committed —
the worked-run notes stay local, and generated configs remain gitignored.

🤖 Generated with Claude Code

https://claude.ai/code/session_011NQYJP23NffEUaATAwcyRV

ayenller and others added 3 commits August 10, 2026 15:52
Cloud SQL for MySQL is MySQL, so the schema-level work is small. What stalls
these migrations is the managed-platform layer, and that is what this module
concentrates on:

- SET GLOBAL fails on every variable the migration cares about — no user holds
  SUPER, not even with cloudsqlsuperuser — so each finding carries the gcloud
  command that actually fixes it, and says so when no fix exists.
- Binary logging is an instance setting (--enable-bin-log, requires automatic
  backups), not a database flag; retention is governed by
  --retained-transaction-log-days.
- binlog_format is not user-configurable at all; Cloud SQL forces ROW.
- --database-flags replaces the whole flag list, so every rendered patch command
  ships with the describe-first warning.
- The Cloud SQL Auth Proxy cannot be used by TiDB Cloud DM, and a Cloud SQL read
  replica cannot be a DM source at all. Both surface in Phase 1 rather than at
  cutover.
- IAM database users and DEFINER clauses naming Cloud SQL principals have no
  TiDB equivalent; lower_case_table_names is immutable after instance creation,
  which turns a case-collision warning into an unfixable-on-the-source blocker.

Implements scan and convert; check and sync are documented with CLI stubs that
exit non-zero, and load is deliberately disabled. Follows the established
rules/ (declarative registries) vs core/ (engines) and collectors vs analyzers
split, so 95% of the logic is covered by 210 offline tests with no database.

Convert is idempotent by construction: rules match a length-preserving mask
that blanks out string literals and comments, so re-running over its own output
is a no-op. Nothing is deleted — originals survive in TISHIFT-REMOVED comments.

Parse validation only reports errors the rewrite introduced. sqlglot's MySQL
dialect cannot parse SRID on a spatial column, and blaming the conversion for
that was a false positive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the module to all five places: the supported-paths table, the skill
slash-command list, the CLI toolkit section, the test list, and the project
structure tree.

The CLI section flags the Auth Proxy / DM incompatibility inline, since that is
the decision that is expensive to revisit later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Executing the module end-to-end against a real Cloud SQL for MySQL 8.4.10
instance surfaced a set of defects and missing rules. Every change here is
backed by an observed failure, not by reading docs.

Correctness fixes:

- utf8/utf8mb3 widening now pins the collation when the source left it
  implicit. `CHARSET=utf8mb3` means utf8mb3_general_ci (case-insensitive) on
  MySQL; rewritten to a bare `CHARSET=utf8mb4` it inherits utf8mb4_bin
  (case-sensitive) on TiDB. That flips equality predicates and changes what a
  UNIQUE key rejects. The clause pattern also swallows a following COLLATE so
  a charset/collation pair is one match, instead of emitting two collations.
- The valid-index query was instance-wide. On a single-schema migration off a
  shared instance it pulled in other databases' tables: 2 in-scope tables
  reported as 6, 12 points deducted instead of 4. Now scoped when a schema is
  given.
- information_schema returns UPPERCASE column names regardless of how the
  SELECT is written; the collector indexed by lowercase and raised KeyError
  against a real server.
- TLS handling was wrong in both directions: `tls: false` passed no ssl
  argument at all, so pymysql negotiated TLS opportunistically; `tls: true`
  with no CA passed `{"ca": ""}`, which demands verification with nothing to
  verify against and killed every connection. Cloud SQL signs each instance
  with a per-instance CA, so system roots can never validate it — encrypted-
  but-unverified is now an explicit, warned-about state.

New rule CSQL-WARNING-14 — foreign keys referencing a non-unique parent index:

A prefix of a unique key is not unique. `PRIMARY KEY (branch_code, debtor_no)`
leaves `branch_code` duplicable, so a child FK on it alone is unbacked. MySQL
8.0.16+ rejects these with ERROR 6125; verified on a 250-table ERP schema that
stops at table 57 on Cloud SQL, while TiDB v8.5.3 creates all 250 and enforces
the FK. Scored at 0 — reported for dump portability, not as a TiDB blocker.

Rescored CSQL-WARNING-11 to 0: binlog retention is governed by the instance's
transactionLogRetentionDays, which is not exposed over the MySQL protocol and
does not track `binlog_expire_logs_seconds` (observed: instance raised 1 -> 7
while the variable stayed at 86400). Penalizing a value we cannot read is
wrong; the check now tells the operator to verify with `gcloud`.

Also: `--format` accepts comma-separated values, convert refuses a DDL file
with no CREATE TABLE (a silently failed mysqldump otherwise reads as a clean
zero-hit report), auto-increment detection reads COLUMNS.EXTRA instead of the
cached TABLES.AUTO_INCREMENT counter, and the binlog-enable guidance names the
Console's "Point-in-time recovery" wording and the instance restart it causes.

Gitignores the module's two internal notes — the Chinese design rationale and
the worked-run runbook — alongside the existing aurora-to-tidb entries. The
runbook quotes live hostnames, a GCP project, and a cluster id verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011NQYJP23NffEUaATAwcyRV
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant