feat: use block/mysql (driver name "block-mysql") - #1320
Conversation
block/mysql is now a hard fork with its own module path rather than a replace-target for go-sql-driver/mysql, so SchemaBot imports it directly and opens pools under the name it registers, "block-mysql". This also unblocks the spirit bump: spirit's pkg/dbconn moved to block/mysql and retired its own copy of the RDS certificate bundle, and a tls=rds DSN from EnhanceDSNWithTLS only resolves for a consumer using the same driver package's TLS registry. Both MySQL drivers stay linked, and that is not incidental: go-mysql/hotswap-dsn-driver embeds upstream go-sql-driver and cannot be pointed at the fork, so the credential-reloading storage pool keeps returning upstream's *mysql.MySQLError while every pool SchemaBot opens itself returns the fork's. The names differ, so registration does not collide -- verified: sql.Drivers() reports all three of block-mysql, mysql and mysql-hotswap-dsn. What does not survive that split is errors.As. The two MySQLError structs are field-identical but live in different packages, so asserting one type silently returns false for the other -- and silently is the problem. A retry classifier checking only one type does not fail loudly; it stops recognizing deadlocks and starts surfacing them as permanent errors. So error codes are now read through mysqlerr.Number/Is, which accepts either, and no call site asserts a driver's error type: - pkg/storage/internal/sqlstore/error_classifier.go -- the storage pool, which is exactly the pool that can be opened either way - pkg/mysqlerr.Reason - pkg/engine/spirit.isLockWaitTimeout pkg/mysqlerr/number_test.go pins both directions, including a test asserting the two types are *not* interchangeable, so if a future dependency change ever merges them the second branch is reported as dead rather than left looking like superstition. postgresconn's rdsRootPool took spirit's deleted GetEmbeddedRDSBundle. It now takes mysql.RDSTLSConfig().RootCAs, which is the pool it wanted anyway: the roots are the same because RDS issues from the same private Amazon CAs regardless of engine, and RDSTLSConfig clones per call so nothing is aliased with the MySQL side. TestOpenNormalizesRDSDSNBeforeOpening asserted the driver name "mysql", which now belongs to upstream -- opening under it would silently bypass the fork rather than fail, so the assertion is worth keeping rather than deleting. The sadscan annotations cover pre-existing false positives -- a doc-comment URI shape with literal user:pass placeholders, obviously-fake test fixtures, and three prose comments containing the word "PlanetScale" -- that this change put into a commit for the first time by touching those files' imports. Each was verified byte-identical on main. Verified: go build ./..., go vet ./... and gofmt clean; go test ./pkg/... fully green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Several test cleanups run SQL using t.Context() inside t.Cleanup, which is typically canceled at cleanup time and can silently skip cleanup and leave databases/tables behind.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates SchemaBot to use the github.com/block/mysql fork directly (registered as the database/sql driver name block-mysql) while still allowing the upstream go-sql-driver/mysql to remain linked via the hot-swap DSN driver. To avoid silent retry-classification regressions caused by two non-interchangeable MySQLError types, it routes MySQL error-code inspection through a new shared helper.
Changes:
- Switches MySQL driver imports and
sql.Opencalls across the codebase from"mysql"to"block-mysql"where SchemaBot expects to use the fork. - Introduces
pkg/mysqlerr.Number/pkg/mysqlerr.Isto read MySQL error codes from either linked driver’s error type and updates retry/lock-wait classification call sites accordingly. - Updates Postgres RDS CA handling to source roots from
block/mysqlnow that Spirit no longer embeds/exposes its own bundle; bumpsblock/spiritandblock/vitesspins accordingly.
File summaries
| File | Description |
|---|---|
| pkg/webhook/webhook_misc_integration_test.go | Use block-mysql for target DB setup in webhook integration tests. |
| pkg/webhook/webhook_integration_test.go | Switch test DB opens to block-mysql; includes MySQL container + service setup helpers. |
| pkg/webhook/vschema_only_check_integration_test.go | Use block-mysql for schemabot storage DB in vschema-only check tests. |
| pkg/webhook/terminal_apply_head_publish_test.go | Use block-mysql for schemabot storage DB in terminal publish test. |
| pkg/webhook/rollback_integration_test.go | Use block-mysql and forked mysql package in rollback integration tests. |
| pkg/webhook/plan_integration_test.go | Use block-mysql for target + storage DB access in plan integration tests. |
| pkg/webhook/plan_drift_integration_test.go | Use forked mysql package and block-mysql in drift integration tests. |
| pkg/webhook/plan_comment_retire_integration_test.go | Use block-mysql for schemabot storage DB in plan comment retirement tests. |
| pkg/webhook/plan_change_ownership_integration_test.go | Use block-mysql in drift + storage DB access during ownership tests. |
| pkg/webhook/fanout_two_deployment_integration_test.go | Use block-mysql for target + storage DB access in fanout tests. |
| pkg/webhook/failure_logs_integration_test.go | Use block-mysql for schemabot storage DB in failure-log summary tests. |
| pkg/webhook/direct_gate_integration_test.go | Use block-mysql for drift DB access when asserting gate behavior. |
| pkg/webhook/copy_discard_gate_integration_test.go | Use block-mysql for drift DB access in copy/discard gate scenarios. |
| pkg/webhook/control_integration_test.go | Register forked driver and use block-mysql for control-operation tests. |
| pkg/webhook/comment_authority_integration_test.go | Use block-mysql for schemabot storage DB in comment authority tests. |
| pkg/webhook/check_records_stopped_test.go | Use block-mysql for schemabot storage DB in check-record tests. |
| pkg/webhook/check_records_rollback_test.go | Use block-mysql for schemabot storage DB in rollback check-record tests. |
| pkg/webhook/check_records_refused_plan_test.go | Use block-mysql for schemabot storage DB in refused-plan tests. |
| pkg/webhook/blocked_gate_integration_test.go | Use block-mysql for drift DB access in blocked gate tests. |
| pkg/webhook/auto_plan_integration_test.go | Use forked mysql package + block-mysql in auto-plan integration tests. |
| pkg/webhook/apply_integration_test.go | Use block-mysql for target DB in apply integration tests. |
| pkg/webhook/apply_comment_integration_test.go | Use block-mysql for schemabot storage DB in apply-comment lifecycle tests. |
| pkg/webhook/apply_check_records_integration_test.go | Use block-mysql for schemabot storage DB in apply check-record tests. |
| pkg/testutil/mysql.go | Register forked driver for MySQL readiness probes in tests. |
| pkg/tern/shard_writethrough_integration_test.go | Use block-mysql for lease stamping DB in tern integration tests. |
| pkg/tern/local_resume_engine_logging_integration_test.go | Use block-mysql for local client DB operations in tern tests. |
| pkg/tern/local_dispatch_shard_integration_test.go | Use block-mysql for local shard dispatch DB operations. |
| pkg/tern/local_dispatch_attach_integration_test.go | Use block-mysql for attach dispatch DB operations. |
| pkg/tern/local_control_multiop_resume_integration_test.go | Use block-mysql for lease DB in multi-op resume fixture. |
| pkg/tern/local_control_cancel_settle_integration_test.go | Use block-mysql for control cancel/settle DB operations. |
| pkg/tern/local_client.go | Switch driver import to forked mysql package in tern local client. |
| pkg/tern/local_client_integration_test.go | Use forked mysql package + block-mysql across tern local client tests. |
| pkg/tern/local_apply_adopt_integration_test.go | Use block-mysql for local adopt apply fixture DB operations. |
| pkg/tern/grpc_retryable_pause_integration_test.go | Use forked mysql package + block-mysql for control-plane storage setup. |
| pkg/tern/grpc_control_rejection_integration_test.go | Use block-mysql for direct DB writes used in gRPC rejection tests. |
| pkg/storage/internal/sqlstore/webhook_events_test.go | Register forked driver; use block-mysql for storage tests. |
| pkg/storage/internal/sqlstore/retry_test.go | Switch mysql error type import to forked mysql package. |
| pkg/storage/internal/sqlstore/parity_test.go | Register forked driver and use block-mysql for parity harness. |
| pkg/storage/internal/sqlstore/mysql_test.go | Register forked driver and open test DB via block-mysql. |
| pkg/storage/internal/sqlstore/locks_test.go | Use block-mysql for concurrent lock-store test pools. |
| pkg/storage/internal/sqlstore/error_classifier.go | Route MySQL retry classification through pkg/mysqlerr.Is (no driver-type asserts). |
| pkg/storage/internal/sqlstore/error_classifier_test.go | Switch mysql error type import to forked mysql package. |
| pkg/storage/internal/sqlstore/checks_test.go | Use block-mysql for changed-rows store DB creation. |
| pkg/storage/internal/sqlstore/apply_operations_test.go | Use block-mysql for concurrent-driver pools and DB-error cases. |
| pkg/storage/internal/sqlstore/applies_test.go | Use block-mysql for concurrent-driver pools and DB-error cases. |
| pkg/serve/serve.go | Register forked driver for server runtime. |
| pkg/serve/serve_close_test.go | Register forked driver; open lazy handle via block-mysql in close-path test. |
| pkg/postgresconn/postgresconn.go | Source RDS root CA pool from block/mysql TLS config. |
| pkg/pendingdrops/cleaner_integration_test.go | Register forked driver and use block-mysql in pending-drops cleaner tests. |
| pkg/namedlock/namedlock_integration_test.go | Register forked driver in namedlock integration tests. |
| pkg/mysqlerr/number.go | New helper to extract MySQL error codes from either linked driver’s error type. |
| pkg/mysqlerr/number_test.go | Tests for dual-driver error-code extraction and non-interchangeable error types. |
| pkg/mysqlerr/mysqlerr.go | Update Reason to use mysqlerr.Number rather than a single driver type assertion. |
| pkg/mysqlerr/mysqlerr_test.go | Switch mysql error type import to forked mysql package. |
| pkg/mysqlconn/mysqlconn.go | Use block-mysql as the default driver name; document split with hot-swap driver. |
| pkg/mysqlconn/mysqlconn_test.go | Update expectation to block-mysql and parse DSN with forked mysql package. |
| pkg/localscale/tls_integration_test.go | Use forked mysql package + block-mysql for localscale TLS tests. |
| pkg/localscale/server.go | Register forked driver and use block-mysql for vtgate/vtcombo DB pools. |
| pkg/localscale/server_integration_test.go | Register forked driver and use block-mysql for localscale integration DB access. |
| pkg/localscale/server_deploy_integration_test.go | Register forked driver and use block-mysql for localscale deploy tests. |
| pkg/localscale/proxy.go | Use block-mysql for upstream DB opened per proxy client connection. |
| pkg/localscale/planetscale_recovery_integration_test.go | Switch mysql package import to forked mysql package. |
| pkg/localscale/managed.go | Use block-mysql for managed-cluster mysqld connections. |
| pkg/localscale/helpers.go | Use block-mysql for branch DB + backend mysqld connections. |
| pkg/localscale/handlers_branches.go | Use block-mysql when snapshotting branch schemas via mysqld. |
| pkg/inventory/static.go | Switch mysql DSN parsing to forked mysql package. |
| pkg/inventory/static_test.go | Switch mysql DSN parsing to forked mysql package; add sadscan annotations on Postgres DSNs. |
| pkg/inventory/connection_assembler.go | Switch mysql DSN parsing to forked mysql package; add sadscan annotation on PlanetScale token-name constant comment. |
| pkg/inventory/connection_assembler_test.go | Switch mysql DSN parsing to forked mysql package; add sadscan annotation on Postgres DSN assertion. |
| pkg/etre/resolver_test.go | Switch mysql DSN parsing to forked mysql package. |
| pkg/engine/spirit/spirit_integration_test.go | Switch MySQL test DB opens to block-mysql and mysql package to fork. |
| pkg/engine/spirit/helpers.go | Switch mysql package import to forked mysql package. |
| pkg/engine/spirit/failure_reason_test.go | Switch mysql package import to forked mysql package. |
| pkg/engine/spirit/existing_copy.go | Switch mysql package import to forked mysql package. |
| pkg/engine/spirit/direct.go | Switch mysql import to forked mysql and use mysqlerr.Is for lock-wait timeout detection. |
| pkg/engine/spirit/control.go | Switch mysql package import to forked mysql package. |
| pkg/engine/planetscale/tls.go | Switch mysql package import to forked mysql package for TLS config registration. |
| pkg/engine/planetscale/planetscale.go | Switch mysql package import to forked mysql package; open vtgate via block-mysql; add sadscan annotation on PlanetScale lint-error comment. |
| pkg/engine/planetscale/planetscale_test.go | Switch mysql package import to forked mysql package. |
| pkg/engine/planetscale/branch.go | Switch mysql package import to forked mysql package; open keyspace DBs via block-mysql. |
| pkg/engine/planetscale/apply.go | Switch mysql package import to forked mysql package; open branch connection via block-mysql. |
| pkg/api/telemetry_integration_test.go | Register forked driver and use block-mysql for storage DB in telemetry tests. |
| pkg/api/service_integration_test.go | Register forked driver and use block-mysql for storage DB in service integration tests. |
| pkg/api/rollback_plan_integration_test.go | Register forked driver and use block-mysql for storage DB in rollback-plan integration tests. |
| pkg/api/pending_drops_cleaner_integration_test.go | Register forked driver for pending-drops cleaner API integration tests. |
| pkg/api/operator_multi_operation_integration_test.go | Register forked driver for operator multi-operation API integration tests. |
| pkg/api/mysql_shared_integration_test.go | Switch mysql package import to fork; open shared MySQL via block-mysql. |
| pkg/api/ensure_schema_integration_test.go | Register forked driver for ensure-schema integration tests. |
| pkg/api/enqueue_authorized_apply_integration_test.go | Register forked driver and use block-mysql for storage DB in enqueue tests. |
| pkg/api/config.go | Switch mysql DSN parsing import to forked mysql package. |
| pkg/api/config_test.go | Switch mysql DSN parsing import to forked mysql package. |
| integration/workflow_test.go | Use block-mysql for integration workflow DB setup and verification queries. |
| integration/status_cli_test.go | Use block-mysql for schemabot storage DB in status CLI integration tests. |
| integration/setup_test.go | Switch mysql package import to fork; use block-mysql for container schema init and tern storage opens. |
| integration/serve_boot_retry_test.go | Switch mysql package import to forked mysql package. |
| integration/resolve_apply_id_test.go | Use block-mysql for schemabot + target DB access in remote apply-id integration tests. |
| integration/operator_test.go | Use block-mysql for schemabot storage + target DB access in operator integration tests. |
| integration/hybrid_mode_test.go | Switch mysql package import to fork; use block-mysql in hybrid-mode integration tests. |
| integration/grpc_integration_test.go | Switch mysql package import to fork; use block-mysql for schemabot + target DB access. |
| integration/cli_test.go | Switch mysql package import to fork; use block-mysql for CLI integration test DB operations. |
| go.mod | Add github.com/block/mysql; bump github.com/block/spirit; update Vitess replace to block/vitess pin. |
| go.sum | Add checksum entries for block/mysql; update checksums for new Spirit/Vitess pins. |
| e2e/testutil/db.go | Register forked driver; use block-mysql for MySQL test utility helpers. |
| e2e/local/local_test.go | Register forked driver and use block-mysql in local E2E tests. |
| e2e/local/helpers_test.go | Use block-mysql for local E2E test helpers (open/cleanup/fixtures). |
| e2e/local/apply_wait_test.go | Use block-mysql for diagnostics DB access in apply-wait E2E test. |
| e2e/k8s/k8s_test.go | Switch mysql import to fork; use block-mysql for k8s E2E verification/cleanup. |
| e2e/k8s/dataplane_progress_ownership_test.go | Use block-mysql for index existence verification in dataplane progress tests. |
| e2e/grpc/multideploy_test.go | Switch mysql import to fork; use block-mysql for multi-deploy gRPC E2E DB ops. |
| e2e/grpc/helpers_test.go | Register forked driver; use block-mysql for gRPC E2E helpers and cleanup. |
| e2e/grpc/grpc_test.go | Register forked driver; use block-mysql for gRPC E2E setup/cleanup DB access. |
Review details
Suppressed comments (3)
pkg/webhook/webhook_integration_test.go:1022
- This t.Cleanup cleanup executes DROP DATABASE statements with t.Context(), which is typically canceled by the time cleanup runs; that can leave staging/production databases behind. Use a non-canceled cleanup context (optionally with a timeout) for the DROP statements.
pkg/webhook/plan_integration_test.go:815 - This t.Cleanup cleanup executes DROP DATABASE with t.Context(), which is usually canceled by the time cleanup runs; that can leave the test database behind. Use a non-canceled cleanup context (optionally with a timeout) for the DROP.
e2e/k8s/k8s_test.go:532 - This cleanup uses t.Context() inside t.Cleanup; by the time cleanup runs the test context is typically canceled, so the DROP TABLE can be skipped and leave objects behind for later tests. Use a non-canceled cleanup context (optionally with a timeout) for the DROP.
t.Cleanup(func() {
db, err := sql.Open("block-mysql", dsn)
if err != nil {
return
}
defer utils.CloseAndLog(db)
_, _ = db.ExecContext(t.Context(), "DROP TABLE IF EXISTS `"+tableName+"`")
})
- Files reviewed: 110/111 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| targetDB, err := sql.Open("block-mysql", targetDSN+"&multiStatements=true") | ||
| require.NoError(t, err, "open target db") | ||
|
|
There was a problem hiding this comment.
🤖 Half right, and the half that is right is out of scope for this branch.
The t.Context()-in-t.Cleanup problem is real. Verified rather than assumed: inside a t.Cleanup function, t.Context().Err() is already context canceled, because t.Context() is cancelled just before cleanups run. Any SQL issued with it fails, and it fails silently wherever the return is discarded. It is 152 sites across 42 files and predates this branch — usetesting with context-background: true is enabled here, which is what steers people to t.Context() in the first place — so it is tracked separately rather than folded into a driver rename.
The syntax claim is a misread. This file has no DROP DATABASE IF NOT EXISTS. Line 58 is DROP DATABASE IF EXISTS; the IF NOT EXISTS you matched is on the CREATE DATABASE two lines below it, at :51.
| t.Cleanup(func() { | ||
| db, err := sql.Open("mysql", e2eTargetDSN+"&multiStatements=true") | ||
| db, err := sql.Open("block-mysql", e2eTargetDSN+"&multiStatements=true") | ||
| if err == nil { | ||
| _, _ = db.ExecContext(t.Context(), "DROP DATABASE IF EXISTS `"+appDBName+"`") | ||
| _ = db.Close() |
There was a problem hiding this comment.
🤖 Real, and tracked separately. t.Context() is cancelled just before t.Cleanup functions run — verified, ctx.Err() reads context canceled inside the cleanup — so the DROP DATABASE cannot succeed, and silently so because the return is discarded.
Not fixed here: it is 152 sites across 42 files, none of them introduced by this branch, and usetesting with context-background: true is enabled in .golangci.yaml, which is what pushes new tests toward t.Context(). Folding a repo-wide test-context sweep into a driver rename would make both harder to review. It needs a cleanup context with its own timeout, applied everywhere at once.
Every red check traced to one cause: e2e/consumermodule is a second module,
and its go.mod was not regenerated after the parent's dependency graph changed.
CI runs `go test -race -run '^$' ./...` there and the toolchain refused with
"updates to go.mod needed". Unit Tests got through five minutes of real tests
before dying on the same step; Lint was cancelled behind the Build failure
rather than finding anything.
Its vitess replace was also pinned to an older block/vitess SHA than the
parent's, which the comment directly above it says must not happen ("Mirror the
parent module's replace directives; replaces do not propagate across module
boundaries"). Now matching, with block/mysql picked up and spirit moved onto the
same pin as the parent.
go-sql-driver/mysql stays in the graph as an indirect dependency, which is the
intended end state, not leftover: hotswap-dsn-driver embeds it, so both drivers
link, and pkg/mysqlerr is the only package importing it directly — for exactly
that reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
🤖 CI is fixed, and on the review: one finding is a real pre-existing bug, the other is a misread. CI — one root cause, not ten. Every red check came from Its vitess
Go cancels It is not confined to the two files flagged: 152 sites across 42 test files use Still a draft: the spirit and block/vitess pins are branch pins until block/spirit#1221 and block/vitess#23 merge. |
Both blockers landed, in both modules: github.com/block/spirit → 10804bbe (block/spirit#1221 merge commit) vitess.io/vitess → 88d15fda (block/vitess#23 merge commit, release-24.0) `go get github.com/block/spirit@main` resolved to an older revision than the branch pin it replaced — the proxy had not indexed the merge yet — so both are pinned to their merge commits explicitly. The consumer module's vitess replace is byte-identical to the parent's again, which is the invariant its own comment states and which was the source of the CI failure before this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
golangci-lint's modernize check, on the new file. This is the first run where lint actually got to execute — the earlier ones were cancelled behind the consumer-module build failure, so it had never linted this code. Full-repo golangci-lint v2 is clean locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither site was an inline sql.Open("mysql", …) literal, which is what the
original sweep grepped for, so both survived it:
pkg/testutil/mysql.go — wait.ForSQL's driver-name argument
pkg/namedlock/…_test.go — a driver name carried in a table field
The testutil one is what failed CI: it runs inside a testcontainers start hook,
so `unknown driver "mysql"` surfaced as `FAIL github.com/…/pkg/namedlock`
rather than as a bad driver name, before any test ran. It is now a named
constant next to the blank import it has to agree with.
Worth recording why only one of the six packages using that helper failed. Only
pkg/namedlock does not link upstream go-sql-driver:
pkg/namedlock upstream linked = 0 ← failed
pkg/pendingdrops upstream linked = 1
pkg/engine/spirit upstream linked = 1
pkg/storage/internal/sqlstore upstream linked = 1
Everywhere else upstream's init registers "mysql", so the readiness probe
resolved and passed — using upstream's driver, not the fork the blank import
declares. That was true before this PR too. So this is not only a fix for the
red package; it is the point at which all six actually probe with block/mysql.
Verified with real containers: namedlock 10.1s, pendingdrops 7.4s,
sqlstore 22.0s, all ok under -tags=integration. golangci-lint clean on all
three tag variants CI runs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spirit no longer needs the fork — it carries its own pkg/parser in-tree, and its go.mod requires upstream github.com/pingcap/tidb/pkg/parser as a plain indirect with no replace of its own. So the redirect this repo carried "for SPATIAL index support in Spirit v0.13.0" no longer redirects anything anyone reaches: `go mod why` now answers "main module does not need package github.com/pingcap/tidb/pkg/parser". Removed from both modules, since the consumer module mirrors the parent's replaces. The upstream indirect requirement stays; it is only the fork redirection that goes. Verified: build clean, golangci-lint clean on all three tag variants, and pkg/engine/spirit green under -tags=integration with real containers (20.2s) — that being the package that would notice a parser regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
🤖 Adversarial correctness review — This is the consumer end of the retirement wave I looked at in There is one bug, and it is the specific hazard the wave has been circling: the DSN still names a TLS config that only one of the two linked drivers knows about, and the storage pool is opened with the other one.
1 — the storage pool cannot open against RDS (high)
Measured, against the DSN
Why 41/41 is green. On the fix. The tempting one — drop the What does work is mirroring the config into the registry that has to resolve it. Both drivers are linked either way — that is already this PR's stated position in // in mysqlconn, once at init
_ = upstreammysql.RegisterTLSConfig("rds", blockmysql.RDSTLSConfig())Verification is preserved — real roots, correct 2 — neither test can see finding 1 (low)The two tests over this code are individually reasonable and collectively blind, in a way worth fixing alongside the bug:
So the RDS-host case never meets the hot-swap driver in any test. A single case that calls Verified — the pins, the dropped parser replace, the error-code helper, and the sweepBoth repointed pins are the merged commits, on the right branches. Dropping the tidb parser replace does not regress spatial support — I checked rather than assumed, because the deleted comment made an explicit claim about it. The fork does diverge from upstream in 15 files, including And re-rendering, which is where a lost subtype would actually bite — a flattened type is a phantom diff, and Subtypes and SRID survive the round trip, so the replace is genuinely dead weight and removing it is a real simplification. The driver-name sweep is complete in production code. No Mutations. Consumer module.
This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Reviewed 48558c8c. The driver switch itself is clean and the sweep is complete — but please read finding 1 before merging: on this branch the MySQL storage pool cannot open against an RDS host, so the server does not start.
ConnectionDSN injects tls=rds, which lives in block/mysql's registry; OpenReloadable goes through the hot-swap driver, which wraps upstream and rejects the name. Measured: invalid value / unknown config name: rds. Its only caller is openStoragePool on the startup path. CI is green because the break is host-shaped — no job points storage at an RDS address, so nothing in the suite can see it. Note that dropping the tls=rds injection is the wrong fix: it would take the storage pool from failing loudly to connecting in the clear. Mirroring the config into upstream's registry works and keeps verification intact (verified).
mysqlerr.Number is the right answer to the two-linked-drivers problem, and pinning the premise with TestDriverErrorTypesAreNotInterchangeable is the part I'd have asked for. I also checked the dropped tidb parser replace rather than trusting the deleted comment — upstream parses and re-renders every geometry subtype and SRID, so it is genuinely dead weight. Both repointed pins are the merged commits on their correct branches.
Full detail: my review comment.
This stamp was left by Claude Code (claude-opus-5).
|
🤖 Review findings - created by Kiran's code review agent - for schemabot/pull/1320, 48558c8. Verdict: 8 findings — 1 blocking (RDS storage pool cannot open), 6 non-blocking, 1 suggestion. The Go sweep itself is clean: zero Blocking1. SchemaBot cannot open its storage pool against any RDS/Aurora MySQL after this PR. Non-blocking2. AGENTS.md now prescribes the driver this PR removed, and nothing gates it. 3. 4. A persistently read-only target now reports "connection lost" instead of its error code. block/mysql converts errno 1290/1792/1836 to 5. The RDS root-CA guard is now dead code. 6. The comment that justified the sweep is scoped wrong, and it is why the doc surface went unchecked. 7. Undocumented DSN break: General suggestions8. Four unpinned driver-name surfaces. The The one thing that could have broken, verifiedError-type split-brain on the storage retry path — the mechanism most likely to silently break, and it is sound. Verified correct
This review was generated by Claude Code (claude-opus-5). |
Retires github.com/go-mysql/hotswap-dsn-driver, the last thing in SchemaBot's dependency graph that reached upstream go-sql-driver/mysql, and with it the whole two-linked-drivers hazard this PR was working around. Fixes a startup break found in review. ConnectionDSN injects tls=rds, and a tls= value is a *name* that only resolves inside the registry of the driver package that registered it -- Spirit registers "rds" into block/mysql. Open honoured that; OpenReloadable could not, because the hot-swap driver embeds upstream and cannot be pointed at the fork. A MySQL storage pool whose host resolves as RDS failed to open with "unknown config name: rds", on the startup path, so the server did not come up. No CI job points storage at an *.rds.amazonaws.com address, so the break was host-shaped rather than code-shaped and nothing in the suite could see it. Mirroring the config into upstream's registry would have worked, but having one registry is better than keeping two in sync, and dropping the injection instead would have taken the pool from failing loudly to connecting in the clear. pkg/connreload holds the reload machinery, driver-independent: a caller supplies Resolve (raw DSN -> driver.Connector) and Refused (does this dial error mean the server rejected these credentials), and the package owns everything about *when* to reload. That is the subtle part, and it existed twice -- postgresconn had its own copy, which now goes away. Both storage pools resolve their DSN through the same secrets machinery, so a difference in how aggressively they re-resolve it would have said nothing about either engine. The reimplementation is not a port. The driver it replaces kept its reload callback in a package-level variable, so opening a second reloadable pool silently repointed the first one's reload at the second one's secret; it had no cooldown, so a secrets-backend outage cost one resolve per rejected dial; and it pinned the electing dial for the duration of the reload, holding a pool connection slot. Each of those is fixed and pinned by a test. pkg/mysqlerr keeps Number/Is and loses its upstream branch. The helper is still the right seam -- nothing about a second driver fails to compile, and the way it breaks is silent -- so a depguard rule now denies both import paths, with the reason in the message. Also adds the regression test finding 2 asked for: a reloadable pool against an RDS host, asserting the injected TLS name resolves *and* that what the pool dials with verifies the server. Both halves are needed -- block/mysql applies RDS TLS on its own, so the resolved-trust assertions alone cannot tell "the name resolved" from "the driver supplied TLS anyway". github.com/go-sql-driver/mysql remains in go.mod as an indirect requirement: testcontainers-go/modules/mysql test-imports it. No SchemaBot package links it -- verified with go list -deps -test ./... The sadscan annotations in postgresconn_test.go are incidental: the scanner reports every finding in a file once the file is touched, and these are the fake localhost DSNs the suite has always used. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
🤖 Addressed in Your diagnosis was exactly right, including that the tempting fix is the dangerous one. Mirroring So The reimplementation is not a port, and
Finding 2 is the test I added, and your framing of the property to pin — with TLS, not merely opens — is what made it non-vacuous. My first version was not: I asserted the resolved trust and it survived a mutation that dropped the Two things worth your eye on the re-review:
I also took your "green is doing no work here" point into the PR description verbatim in substance — the host-shaped-not-code-shaped distinction is worth a reader seeing. On the Copilot findings: |
|
🤖 Re-review — The high finding is fixed, and fixed at a level I didn't propose. I suggested mirroring Measured against the DSN And the two things I asked for beyond the fix are both here. The It flags test files too, so a test reaching for upstream is caught on the same footing as production code. Two low findings, both about the new scheduler rather than the fix.
1 — the retry loop's termination is emergent, and the failure shape is a spin (low)
I found this by mutation rather than by reading, and the pattern is what makes it worth a line. Of the four mutations I made to the invariants this loop depends on, three produce an infinite loop, and the suite catches them only as a test timeout: The mutations aren't the point; where they land is. This loop runs inside Since 2 — a lagging secret sync costs a full cooldown of failed dials (low)
But the same branch catches the transient case: the rotation has happened on the server and the secret store has not caught up yet. Then the reload legitimately returns the old password, the retry is legitimately refused, and the window is armed. If the store syncs two seconds later, new dials keep failing with 1045 for the remaining ~28s without attempting a reload, because the cooldown check at line 225 runs before anything else. Established connections are unaffected, so this is degraded rather than down — but it is a self-inflicted availability window on the storage pool, triggered by the exact event the package exists to make transparent. The two conditions have different expected recovery times, so they could reasonably have different windows: a short first arm for "credentials refused after a successful reload" (a sync lag clears in seconds) and the full Verified — the extraction, the coverage that moved, the driver graph, and the mutation ledgerThe No test coverage was lost, which a −621-line test diff does not make obvious. All 16 The driver graph is what the change claims. On the history in those comments. Attack that dissolved — Mutation ledger — 7 run, 7 killed (3 of them only by hang; see finding 1). Killed cleanly: dropping the RDS TLS injection (5 tests, including the new RDS TLS test — so the mechanism assertion is load-bearing); Concurrency. Local checks. This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving at 012bed8b. The high finding from round 1 is fixed at the root rather than patched — dropping the second MySQL driver and building the reloadable pool with sql.OpenDB over an in-repo connector means there is no registry lookup to get wrong, so the failure class is gone rather than worked around. Measured: OpenReloadable now opens against an RDS host with verified TLS, through mysql.MySQLDriver.
Both follow-ups I asked for are here and I checked each one bites rather than assuming: the depguard rule fires on both banned imports (test files included), and TestReloadablePoolReachesRDSHostWithVerifiedTLS pins the mechanism as well as the trust chain, which is what keeps a later switch to the driver's own auto-TLS a deliberate decision. No coverage was lost in the −621-line test diff — all 16 reloadable cases moved to pkg/connreload and gained seven more.
Two low findings on the new scheduler, neither blocking: refresh's loop is unbounded and its termination depends on invariants maintained elsewhere (three of four mutations there spin rather than fail, inside Connect, which holds a pool slot), and the cooldown treats a lagging secret sync the same as a secrets-backend outage, costing up to 30s of failed dials during the rotation it exists to make transparent.
This stamp was left by Claude Code (claude-opus-5).
What
block/mysqlis now a hard fork with its own module path, rather than areplace-target forgo-sql-driver/mysql. SchemaBot imports it directly and opens its pools under the name the fork registers,block-mysql.Upstream
go-sql-driver/mysqlis no longer linked by any SchemaBot package. Getting there meant reimplementing the one thing that still pulled it in, which turned out to also be the fix for a startup break.All three dependency pins are merged revisions:
github.com/block/mysqlee0a93fe— mastergithub.com/block/spirit10804bbe— #1221, head ofmainvitess.io/vitess→ block/vitess88d15fda— #23, head ofrelease-24.0Why the spirit bump forces this
Spirit's
pkg/dbconnmoved to block/mysql and retired its own copy of the RDS certificate bundle. Atls=rdsDSN out ofEnhanceDSNWithTLSonly resolves for a consumer using the same driver package's TLS registry — registries are per-package globals, and nothing about them travels in the DSN. On the old pairing (spirit on block/mysql, SchemaBot on upstream) every RDS MySQL connection would have failed inParseDSNwithunknown config name: rds, before any dial.That property is the whole story below, applied one level down.
The bug this fixes
Found in review of an earlier revision of this PR, and it is the same hazard one layer in:
OpenReloadablecould not open a MySQL storage pool against an RDS host, so the server did not start.ConnectionDSNinjectstls=rds.Opendials throughblock-mysql, whose registry has that name.OpenReloadabledialled throughgo-mysql/hotswap-dsn-driver, which embeds upstream and cannot be pointed at the fork — so it parsed the DSN with a registry that has nords:OpenReloadable's only caller isopenStoragePool(pkg/serve/serve.go), on theDialectMySQLarm, during startup.CI was green and that green was doing no work.
tlsModeForHostinjectstls=rdsonly whendbconn.IsRDSHost(addr), and no CI job points storage at an*.rds.amazonaws.comaddress. The break was host-shaped, not code-shaped, so nothing in the suite could see it.Two fixes were available and both were rejected:
rdsinto upstream's registry. Works, and preserves verification. But it keeps two registries in sync forever, and the sync is the thing that just broke.tls=rdsinjection, since block/mysql applies the RDS trust store itself. Equivalent forOpen; for the hot-swap pool it would have taken the storage pool from failing loudly to connecting in the clear, on the connection that carries every credential and lease.The fix taken removes the second driver instead, so there is one registry and the question cannot recur.
pkg/connreload— the reload machinery, driver-independentThe hot-swap driver's job was small: re-read credentials when a dial is refused, so a rotated secret does not need a pod restart. That is now
pkg/connreload. A caller supplies two functions:Resolve(dsn) (driver.Connector, error)— raw DSN to dialer, called once at open and once per reload, never per dial.Refused(error) bool— does this dial error mean the server rejected these credentials.Everything about when to reload lives in the package: one reload in flight at a time, one reload per generation of credentials however many dials failed against it, a cooldown so a secrets-backend outage is not amplified into one resolve per refused dial, and a reload that runs detached so a hung secret resolution cannot pin a pool connection slot.
It is one implementation because that scheduling is the subtle part, and it existed twice —
postgresconnhad its own copy, which this deletes. Both storage pools resolve their DSN through the same secrets machinery, so a difference in how aggressively they re-resolve it would have said nothing about either engine. MySQL's half is now 60 lines; PostgreSQL's is 35.It is not a port. Three things the replaced driver got wrong, each now pinned by a test:
pkg/connreloadpkg/mysqlerrand keeping one drivermysqlerr.Number/Isread a MySQL error code without asserting a driver's error type. With upstream gone the second branch is deleted, but the helper stays, because the failure mode it guards is silent: twoMySQLErrorstructs in two packages are field-identical and unrelated undererrors.As, so a classifier that type-asserts keeps compiling and just stops recognizing deadlocks.Since nothing fails to compile if a second driver returns, a
depguardrule now denies both import paths with the reason in the message. That is the only module-wide enforcement available for this, and it is one line of config per denied path — happy to drop it if you would rather not add the linter.Call sites that read codes:
sqlstore/error_classifier.go,mysqlerr.Reason,engine/spirit.isLockWaitTimeout. None assert a driver type.Also here
replaceis dropped. Spirit no longer needs the fork; upstream511dba1dbe17parses and re-renders every geometry subtype andSRID.postgresconn.rdsRootPooltakesmysql.RDSTLSConfig().RootCAs— spirit deletedGetEmbeddedRDSBundlewith its bundle. Same private Amazon roots regardless of engine, andRDSTLSConfigclones per call, so nothing is aliased with the MySQL side.Scope
Mostly mechanical: the import path in ~70 files and the driver name at ~250
sql.Opensites, the large majority in tests.Two things deliberately not renamed, because they are not driver names:
topo.OpenServer("mysql", …)— a Vitess topo implementation name.sqlx.NewDb(db, "mysql")— a bindvar-dialect tag. sqlx's registry only knowsmysql/sqlite3/nrmysql/nrsqlite3;"block-mysql"resolves toUNKNOWNand silently breaks named-query binding. (SchemaBot has no such call, but GAP does — flagging it since the same sweep runs there.)Verification
go build ./...,go vet ./...(compiles all test packages),gofmt -lclean,golangci-lint runclean on the default,integrationande2etag variantsgo test ./...green;-racegreen onconnreload,mysqlconn,postgresconn,mysqlerr,storage/...,servego list -deps -test ./...contains nogithub.com/go-sql-driver/mysql. It remains ingo.modas// indirectbecausetestcontainers-go/modules/mysqltest-imports it; no SchemaBot package does.TestReloadablePoolReachesRDSHostWithVerifiedTLSasserts both halves: that the injected TLS name resolves in the driver the pool dials with, and that what the pool dials with verifies the server — real roots, correctServerName,InsecureSkipVerify=false, no plaintext fallback. Both halves are needed, because block/mysql applies RDS TLS on its own, so the resolved-trust assertions alone cannot distinguish "the name resolved" from "the driver supplied TLS anyway".connreload: removing the cooldown check, removing the stale-generation arm guard, running the reload inline instead of detached, removing the stale-generation dedup, removingrecover(), ignoring theRefusedpredicate, never advancing the generation, never clearing the cooldown on success. Inmysqlconn: dropping the RDS TLS injection, and weakening the mode toPREFERRED.gofmt -lonmainconfirmed clean first, so the reformatted files are import-ordering from this change and nothing elseThe
sadscan:disableannotations cover false positives: obviously-fakelocalhosttest DSNs, a doc-comment URI shape with literaluser:passplaceholders, and prose comments containing the word "PlanetScale". The scanner reports every finding in a file once the file is touched, which is why untouched lines inpostgresconn_test.goacquired annotations.Related
🤖 Generated with Claude Code