Skip to content

refactor(stdlib): remove mysql2 native binding - #10680

Draft
proggeramlug wants to merge 3 commits into
wip/10661-dyn-eval-class-exprfrom
wip/mysql2-removal
Draft

proggeramlug wants to merge 3 commits into
wip/10661-dyn-eval-class-exprfrom
wip/mysql2-removal

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Removes the mysql2 native binding so import mysql from "mysql2" resolves to
the real npm package, per the owner's decision to stop shipping hand-written
Rust reimplementations of npm packages (they drift from the real thing — see
the jsonwebtoken.verify example in the campaign background).

Must not merge before #10675 (wip/10661-dyn-eval-class-expr, this PR's
base). mysql2's real source does not compile without that PR's dyn_eval
class-expression support — generate-function builds mysql2's row parsers
with a runtime new Function(...) that returns a class expression.

What was removed and why

Two candidate locations turned out to be genuinely separate, both hand-written
reimplementations of mysql2 using sqlx directly:

  • crates/perry-ext-mysql2 — the governance-tracked binding
    (docs/src/native-libraries/governance.md: "Source package", target
    "Compile the upstream package source"). Wired into
    well_known_bindings.toml's [bindings.mysql2] / [bindings."mysql2/promise"],
    and the crate a plain import mysql from "mysql2" (no compilePackages)
    actually linked against.
  • crates/perry-stdlib/src/mysql2/ (~1870 lines) — a second,
    independent mysql2 implementation gated behind perry-stdlib's
    bundled-mysql2 feature (default-on via full). It defined the exact same
    js_mysql2_* symbol names as perry-ext-mysql2, in a separate handle
    registry
    (perry-ffi's handle table and perry-stdlib's common::handle
    table are explicitly documented as disjoint integer spaces — see
    crates/perry-ffi/src/handle.rs). Its dispatch_mysql2_method/
    dispatch_mysql2_property dynamic-dispatch fallback (for Drizzle's
    interface-typed client fields) could only ever resolve handles created by
    its own js_mysql2_create_connection/createPool — which lose the link
    to whichever archive the linker picks between two archives exporting the
    same symbol names. In the default (full) feature build both crates ship
    js_mysql2_*, so this was already a live footgun, not merely inert.

Both were removed. Neither is separable from "the mysql2 native binding"
— they are the same category of hand-written duplicate the campaign targets,
just delivered through two different mechanisms (well-known ext-crate flip vs.
a default-on stdlib feature).

Also removed the bundled-mysql2 HIR heuristic in
perry-hir/src/lower/expr_call/native_module.rs (mysql2_config_signature /
detect_bundled_mysql2_create) that recognized a bundler-inlined
(webpack/turbopack) createPool/createConnection call by its config-object
shape and routed it to perry-ext-mysql2's FFI symbols. That workaround
existed only because the AOT binary couldn't run mysql2's generate-function
row parsers; #10675 fixes that generally via dyn_eval, so the heuristic (and
its bundled_mysql2_tests module, trimmed rather than deleted — it also
covered an unrelated is_process_active_array_helper test, kept under a
renamed native_module_helper_tests mod) is no longer needed.

Left alone, deliberately: a handful of shared perry-hir files
(local_natives.rs, native_new.rs, native_fetch.rs, misc.rs,
module_decl.rs, static_and_instance.rs) still pattern-match on the string
literals "mysql2" / "mysql2/promise" for class-shape inference (e.g.
tagging createPool()'s result as a Pool). These are now structurally
unreachable — the only two producers of a NativeMethodCall/NativeModuleRef
with that module string were NATIVE_MODULES membership (removed) and the
bundled-heuristic above (removed) — but hand-editing every multi-package match
arm in files shared with the concurrent axios/pg removal branches risked a
larger, conflict-prone diff for no behavioral change. Also left alone:
prose comments across perry-runtime/perry-codegen that cite real mysql2
source shapes (MockBuffer, generate-function, drizzle transactions) as
motivating examples for unrelated compiler features — those describe why the
compiler behaves a certain way, not the binding being removed.

Registries and gates touched

  • crates/perry/well_known_bindings.toml — drop [bindings.mysql2] /
    [bindings."mysql2/promise"]
  • crates/perry-api-manifest/src/entries.rs (+ entries/part_1.rs,
    entries/part_3.rs) — drop NATIVE_MODULES entries and all mysql2
    manifest rows
  • crates/perry-codegen/src/lower_call/native_table/databases.rs — drop the
    MySQL2 NativeModSig section
  • crates/perry-codegen/src/ext_registry.rs — drop the mysql2 FFI_REGISTRY
    rows
  • crates/perry/src/commands/stdlib_features.rs,
    optimized_libs/driver.rs, optimized_libs/freshness.rs — drop the
    mysql2bundled-mysql2 wiring
  • crates/perry/src/commands/compile/resolve.rs
    PERRY_NATIVE_EXTENSION_PACKAGES (this is what let mysql2's real .js
    source reach the module walker instead of being silently skipped as
    "handled by native stdlib")
  • workspace-architecture.json — drop the perry-ext-mysql2 decision entry
    and refresh the baseline counts (workspace_members, externalize)
  • scripts/unrooted_local_shape_baseline.json,
    scripts/string_payload_access_baseline.txt,
    scripts/native_result_ledger.tsv (+ EXPECTED_ROWS/EXPECTED_PROVIDERS
    in native_result_ledger.py) — drop entries for the deleted files/symbols
  • .github/workflows/test.yml, scripts/run_doc_tests.sh/.ps1 — drop the two
    explicit -p perry-ext-mysql2 cargo build args (compile-smoke + UI-backend
    jobs, doc-tests harness); these would otherwise fail with "no such package"
  • crates/perry-ui-android/src/stdlib_stubs.rs — drop the 14 js_mysql2_*
    Android stub exports
  • docs/src/api/reference.md, docs/api/perry.d.ts — regenerated
    (--print-api-manifest)
  • docs/src/native-libraries/governance.md — regenerated table (drops the
    perry-ext-mysql2 row) + a new "Completed source migrations" bullet,
    matching the existing slugify entry's pattern
  • crates/perry/src/commands/compile/collect_modules/binding_faithfulness.rs
    — dropped lookup_preserves_registered_subpaths_before_falling_back, the
    one test whose only real-world fixture (mysql2/promise's
    alias-of = "mysql2") no longer exists in well_known_bindings.toml (it
    was the only root+subpath alias in the whole table); the sibling test
    using "mysql2" as an arbitrary on-disk-directory fixture name is untouched
    since it doesn't depend on the registry.

Acceptance test: real query round-trip, no compilePackages entry

Built on the perrymaster host (--profile perry-dev, -p perry -p perry-runtime-static -p perry-stdlib-static, then PERRY_WORKSPACE_ROOT set
so auto-optimize could pull in the wrapper crates the real mysql2 source
needs — perry-wasm-host, perry-ext-net, perry-ext-events,
perry-ext-zlib). Test program:

import mysql from "mysql2/promise";
const conn = await mysql.createConnection({ host: "127.0.0.1", port: 3306, user: "pkgaudit", password: "pkgaudit_pw", database: "perry_pkgaudit_test" });
await conn.query("CREATE TABLE IF NOT EXISTS t (id INT PRIMARY KEY, val VARCHAR(64))");
await conn.query("INSERT INTO t (id, val) VALUES (1, 'hello-perry')");
const [rows] = await conn.query("SELECT val FROM t WHERE id = 1");
// ...DROP TABLE, conn.end()

with package.json:

{ "dependencies": { "mysql2": "^3" } }

No perry.compilePackages entry at all — not for mysql2, not for its
transitive deps. Compile log: Compile package wildcard: expanded to 60 installed package(s) (Perry's default automatic package-routing path,
already used by the slugify migration) compiled real mysql2 + its
dependency tree (generate-function, iconv-lite, long, lru.min,
named-placeholders, sql-escaper, aws-ssl-profiles, ...) from source.
One informational notice: generate-function/index.js:362's runtime new Function(...) — mysql2's row-parser factory — was "handled at runtime:
runtime interpreter (#6559)", i.e. dyn_eval, exactly the mechanism #10675
added.

Ran the resulting binary against the real local MySQL 8.0.46 server:

GOT_ROW=[{"val":"hello-perry"}]
RESULT: PASS

CREATE TABLE / INSERT / SELECT / DROP TABLE all round-tripped with the
real row parser, real wire protocol, real auth handshake.

Verification

  • cargo check/cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static: clean (confirmed .a mtimes
    moved after the edits)
  • cargo test -p perry-api-manifest -p perry-hir: all passing
  • cargo test -p perry-codegen --test manifest_consistency: all 5 passing
    (every_native_module_has_at_least_one_manifest_entry,
    every_dispatch_entry_has_manifest_counterpart, etc.) — confirms the
    registry edits are internally consistent
  • python3 scripts/binding_governance.py --check: OK (39 extension crates
    classified)
  • node scripts/binding_pins.mjs --check: OK (37 pinned, lock-step holds)
  • scripts/run_lint_gates.sh (SKIP_COMPILE_GATES=1): 76 of 77 passed.
    The one failure, "Public benchmark evidence freshness", is pre-existing on
    every PR in this repo (per the campaign's own fix-agent contract) — not
    touched here.
  • Real MySQL round-trip: see above.

Not run / out of scope

  • Compile tier of run_lint_gates.sh (known-red on Linux per the campaign
    contract; not run)
  • Full gap suite (host stalls under auto-optimize per the campaign contract);
    ran the targeted registry/consistency tests instead
  • No version bump / CLAUDE.md edit — per this campaign's convention, the
    maintainer bumps at merge time

Ralph Küpper added 2 commits September 18, 2026 23:11
Deletes perry-ext-mysql2 (the well-known-table crate that provided
js_mysql2_* symbols for plain `import mysql from "mysql2"`) and
perry-stdlib/src/mysql2/ (a second, duplicate hand-written mysql2
implementation gated behind the bundled-mysql2 feature). Removes the
supporting registry wiring: well_known_bindings.toml, NATIVE_MODULES /
manifest entries in perry-api-manifest, the native_table/databases.rs
MySQL2 codegen rows, ext_registry.rs FFI routing, the bundled-mysql2
HIR heuristic for bundler-inlined mysql2 (superseded by #10675's
dyn_eval class-expression support), stdlib_features.rs /
optimized_libs driver+freshness wiring, workspace-architecture.json,
and the unrooted-local-shape baseline entries for the deleted files.

Based on wip/10661-dyn-eval-class-expr (#10675) — mysql2 does not
compile from real source without that fix.
…docs)

- cargo fmt after the removal (entries.rs / resolve.rs realignment)
- drop the two explicit -p perry-ext-mysql2 args from .github/workflows/test.yml
  (compile-smoke + UI-backend jobs) and from run_doc_tests.sh/.ps1
- drop the 14 js_mysql2_* rows from scripts/native_result_ledger.tsv and
  update its EXPECTED_ROWS/EXPECTED_PROVIDERS counts (343/308)
- refresh workspace-architecture.json's baseline counts (82 members,
  externalize=32) so scripts/workspace_architecture.py --check passes
- rewrite scripts/string_payload_access_baseline.txt via --write-baseline
  (perry-ext-mysql2 row dropped, perry-stdlib 40 -> 38)
- regenerate docs/src/api/reference.md, docs/api/perry.d.ts, and
  docs/src/native-libraries/governance.md's generated table; add a
  "Completed source migrations" entry for mysql2

run_lint_gates.sh (SKIP_COMPILE_GATES=1): 76/77 passed, only the
pre-existing "Public benchmark evidence freshness" red.
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Heads-up before this is queued: this PR's recorded workspace baseline is stale and will fail workspace_architecture.py --check on rebase.

It records workspace_members 82 / externalize 32 / keep 45. Main (023dc0b653) is at 80 / 31 / 44, so a removal landing on it must produce 79 / 30 / 44 — not 82. Five sibling removal PRs carry the identical 82/32/45, which is also mutually impossible: six different crates cannot all produce the same transition.

Full table and reasoning in #10739. The short version, for whoever rebases this:

  • Recompute from the resolved tree and let workspace_architecture.py --check --print-summary reproduce the number independently. Do not derive it from 83 by arithmetic, and do not copy a sibling's figure or one quoted in a comment — they all go stale as the queue advances.
  • The same hazard applies to scripts/native_result_ledger.tsv, scripts/string_payload_access_baseline.txt and the governance/pins tables. Regenerate with their own scripts rather than resolving by hand.
  • MERGEABLE will not catch this. The counts sit on different JSON lines from the deleted crate entry, so git auto-merges both sides without a conflict — chore(bindings): remove axios native binding, compile real axios from source #10679 rebased onto current main today, came out MERGEABLE, and still carried 82/32/45.

Also relevant to the acceptance run whenever it happens: #10735 is live on main — require.main === module is true in every compiled CommonJS module, so any dependency with a CLI entry guard runs its CLI branch when merely imported. A fix is in flight. If acceptance fails in a way that looks like the package misbehaving at import time, test a dependency-free fixture that never mentions the package before attributing it to this removal.

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