Skip to content

fix(codegen+runtime): #489 — drizzle + MySQL e2e (INSERT/UPDATE/DELETE round-trip)#847

Open
proggeramlug wants to merge 2 commits into
mainfrom
worktree-issue-489-mysql
Open

fix(codegen+runtime): #489 — drizzle + MySQL e2e (INSERT/UPDATE/DELETE round-trip)#847
proggeramlug wants to merge 2 commits into
mainfrom
worktree-issue-489-mysql

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

Drives the #489 acceptance program (drizzle + @perryts/mysql against real MySQL) past two blockers so INSERT / UPDATE / DELETE complete end-to-end, with output byte-for-byte matching the tsx baseline and matching row state in the database.

  • c8bb98dcrates/perry/src/commands/compile.rs: transitive parent-class closure picks the canonical defining path instead of the first BTreeMap match by name. mysql-proxy/session.js's this.client(...).then(({rows}) => rows) chained MySqlPreparedQuery → QueryPromise; the closure picked the drizzle-orm/index.js re-export barrel over query-promise.js (alphabetic order), and the link failed with Undefined symbols: _perry_method_…_index_js__QueryPromise__then. New class_canonical_path: HashMap<ClassId, String> populated only from each module's own hir.classes Vec; the parent-closure search prefers the canonical key. Sibling of Class method body not executed when class is imported from another module #83 / Linker: unresolved _perry_fn_..._render for V8-fallback modules referenced from main #678 / fix(codegen): #678 — re-export rename resolves to origin export name #785 — for export * star-re-exports through the transitive parent path, which the namespace-import enumeration already handles correctly (the comment at compile.rs:4032-4045 warns about this exact failure mode).

  • 78300adcrates/perry-codegen/src/type_analysis.rs + crates/perry-runtime/src/object.rs: .then / .catch / .finally on a Promise returned from a class field or method now actually resumes the await chain. Two coordinated gaps:

    1. is_promise_expr didn't see obj.field(args) / obj.method(args) as Promise when typed (…) => Promise<T> or declared async. New arms walk static_type_of(callee) for class-field Function types and ctx.classes[recv].methods (with parent chain) for async/return-Promise methods. Also extended the LocalGet callee branch.
    2. js_native_call_method had no Promise-receiver intrinsic for then/catch/finally. Added an early arm that unboxes the promise handle and dispatches to js_promise_then/_catch/_finally. The closure-arg extractor handles both ABIs perry uses for callbacks crossing this boundary: NaN-boxed POINTER_TAG | (ptr & 0x0000_FFFF_FFFF_FFFF) from js_closure_alloc_singleton, and raw *ClosureHeader bit-cast to f64 from js_assimilate_thenable (see promise.rs:2438-2442 — the await wrapper passes resolve/reject as bare pointers in double slots through user then(resolve, reject) methods).

Version bumped to 0.5.915; CHANGELOG.md has the per-version detail.

Validation

Step Result
cargo build --release -p perry-runtime -p perry-stdlib -p perry clean
cargo test --release -p perry --bin perry 223 / 223 ok
cargo test --release -p perry-runtime 250 / 250 ok
50-line drizzle+@perryts/mysql+drizzle-orm/mysql-proxy against MySQL 9.6 on 127.0.0.1:3306: INSERT/UPDATE/DELETE byte-for-byte match vs tsx, DB state matches

Remaining for #489 acceptance

  • SELECT arm — drizzle's applyMixins(MySqlSelectBase, [QueryPromise]) runtime prototype-copy pattern. Perry's static class table doesn't model methods added at module init via Object.defineProperty(baseClass.prototype, name, ...), so await db.select().from(users) doesn't recognize the mixed-in then and unwraps to the receiver instead of the rows. Separate compat gap; filing as a follow-up under Drizzle: 50-line program against real MySQL via @perry/mysql (followup to #488) #489.
  • crypto.publicEncrypt (Compile-time error for unimplemented Node / Web APIs #463) — needed for caching_sha2_password first-time auth on non-TLS connections. The integration test side-steps this by warming the MySQL server's auth cache via the mysql CLI before running perry, plus PERRY_ALLOW_UNIMPLEMENTED=1 at compile time. Real fix is its own larger piece.

Test plan

  • CI green: lint, cargo-test, parity, compile-smoke, api-docs-drift, security-audit.
  • Spot-check that the byte-for-byte verification reproduces on the maintainer's box (homebrew MySQL 9 with a caching_sha2_password user; pre-warm cache with the mysql CLI; compile with PERRY_ALLOW_UNIMPLEMENTED=1).
  • Confirm drizzle-sqlite + hono-basic fixture failures on main are unchanged (pre-existing, unrelated _perry_wrap_perry_fn_…__textDecoder / __PATH_ERROR link errors).

…defining path

When mysql-proxy/session.js calls `.then(...)` on a Promise, perry's
transitive parent-class closure pulls `QueryPromise` in via the
`MySqlPreparedQuery extends QueryPromise` chain. The closure was
scanning `exported_classes` (a BTreeMap<(path, name), &Class>) with a
name-only `find`. The propagation loop above stamps each re-exporter's
path under the same name, so `export * from "./query-promise.js"` in
drizzle's `index.js` produces two keys for one class — and BTreeMap
alphabetical order returns `index_js` first. The downstream codegen
then emitted `perry_method_<index_js>__QueryPromise__then` extern
declarations + dispatch references in session.js while the symbol is
defined under `perry_method_<query_promise>__QueryPromise__then` —
undefined-symbol link error.

Sibling of #83 / #678 / #785, but for the `export *` re-export path
through the transitive parent closure rather than the consumer's own
import. The namespace-import enumeration path at the same file
(compile.rs:4046-4072) already handled this correctly by iterating
`ctx.native_modules` directly; the parent-closure path had not been
hardened the same way.

Fix: new `class_canonical_path: HashMap<ClassId, String>` populated
only from each module's own `hir_module.classes` Vec (the defining
file, never a re-export). In the parent-closure search, prefer the
BTreeMap entry whose key matches the canonical path; fall back to the
old first-match for classes that exist only via re-exports.

Validation: drizzle + @perryts/mysql + drizzle-orm/mysql-proxy 50-line
program now compiles clean and reaches MySQL on the wire — TCP
connect, handshake, schema DDL, prepared INSERT with the right SQL +
params. `cargo test --release -p perry` clean (223 tests pass).
Pre-existing drizzle-sqlite + hono-basic link failures on main are
unchanged by this patch (separate top-level-const re-export bug).

Remaining for #489 acceptance: (a) `await db.insert(...).values(...)`
never settles in the perry binary — proxy callback returns correctly,
but the result doesn't flow back through `queryWithCache → execute →
QueryPromise.then`; separate runtime bug, not this codegen issue.
(b) `crypto.publicEncrypt` (#463) needed for caching_sha2_password
first-time auth on non-TLS connections. Both filed as follow-ups.

Refs #489.
… resumes await chain

Drizzle + @perryts/mysql INSERT against MySQL: the proxy callback fired
and sent the SQL on the wire correctly, but `await
db.insert(...).values(...)` never settled — execution exited code 0
before the next line. Two gaps fed the same symptom.

(1) `is_promise_expr` didn't recognize `obj.field(args)` /
    `obj.method(args)` as Promise even when typed as
    `(…) => Promise<T>` or `async`. Added arms that consult
    `static_type_of(callee)` for class-field Function types, and walk
    `ctx.classes[recv].methods` (with parent chain) to spot
    `is_async`/return-Promise class methods. Also extended the
    `LocalGet` callee branch to match return-Promise function types.
    Covers `this.client(...)` on a RemoteCallback field,
    `this.pre.execute()` on an async method, `this.session.all(q)` on a
    method returning Promise.

(2) `js_native_call_method` had no `then`/`catch`/`finally` arm for
    Promise receivers. The dispatch fallback ate the missed compile-
    time fast-path and returned undefined, leaving the await chain's
    resolver disconnected from the underlying promise. Added an early
    arm that unboxes the promise handle and dispatches to
    `js_promise_then`/`_catch`/`_finally`. Closure-arg extractor
    handles both ABIs: NaN-boxed `POINTER_TAG | (ptr & 0x0000…ffff)`
    AND raw `*ClosureHeader` bit-cast to f64 (the convention used by
    `js_assimilate_thenable` at promise.rs:2438-2442 when it
    propagates `then(resolve, reject)` callbacks through a user
    `then` method).

Validation: 50-line drizzle+@perryts/mysql+drizzle-orm/mysql-proxy
program against real MySQL 9.6 — INSERT/UPDATE/DELETE round-trip
matches `tsx` baseline byte-for-byte, MySQL row state after the perry
run is identical (alice→alice2 rename committed, bob deleted).
`cargo test -p perry` 223/223, `cargo test -p perry-runtime`
250/250. SELECT arm still blocked separately by drizzle's
`applyMixins(MySqlSelectBase, [QueryPromise])` runtime
prototype-copy pattern (perry's static class table doesn't model
methods added via `Object.defineProperty(baseClass.prototype, ...)`).
Filed as a follow-up under #489.

Refs #489.
@proggeramlug proggeramlug force-pushed the worktree-issue-489-mysql branch from 78300ad to 9a66749 Compare May 16, 2026 11:56
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