fix(codegen+runtime): #489 — drizzle + MySQL e2e (INSERT/UPDATE/DELETE round-trip)#847
Open
proggeramlug wants to merge 2 commits into
Open
fix(codegen+runtime): #489 — drizzle + MySQL e2e (INSERT/UPDATE/DELETE round-trip)#847proggeramlug wants to merge 2 commits into
proggeramlug wants to merge 2 commits into
Conversation
…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.
78300ad to
9a66749
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Drives the #489 acceptance program (drizzle +
@perryts/mysqlagainst real MySQL) past two blockers so INSERT / UPDATE / DELETE complete end-to-end, with output byte-for-byte matching thetsxbaseline and matching row state in the database.c8bb98d —
crates/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'sthis.client(...).then(({rows}) => rows)chainedMySqlPreparedQuery → QueryPromise; the closure picked thedrizzle-orm/index.jsre-export barrel overquery-promise.js(alphabetic order), and the link failed withUndefined symbols: _perry_method_…_index_js__QueryPromise__then. Newclass_canonical_path: HashMap<ClassId, String>populated only from each module's ownhir.classesVec; 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 — forexport *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).78300ad —
crates/perry-codegen/src/type_analysis.rs+crates/perry-runtime/src/object.rs:.then/.catch/.finallyon a Promise returned from a class field or method now actually resumes the await chain. Two coordinated gaps:is_promise_exprdidn't seeobj.field(args)/obj.method(args)as Promise when typed(…) => Promise<T>or declaredasync. New arms walkstatic_type_of(callee)for class-field Function types andctx.classes[recv].methods(with parent chain) for async/return-Promise methods. Also extended theLocalGetcallee branch.js_native_call_methodhad no Promise-receiver intrinsic forthen/catch/finally. Added an early arm that unboxes the promise handle and dispatches tojs_promise_then/_catch/_finally. The closure-arg extractor handles both ABIs perry uses for callbacks crossing this boundary: NaN-boxedPOINTER_TAG | (ptr & 0x0000_FFFF_FFFF_FFFF)fromjs_closure_alloc_singleton, and raw*ClosureHeaderbit-cast to f64 fromjs_assimilate_thenable(seepromise.rs:2438-2442— the await wrapper passes resolve/reject as bare pointers in double slots through userthen(resolve, reject)methods).Version bumped to 0.5.915; CHANGELOG.md has the per-version detail.
Validation
cargo build --release -p perry-runtime -p perry-stdlib -p perrycargo test --release -p perry --bin perrycargo test --release -p perry-runtime@perryts/mysql+drizzle-orm/mysql-proxyagainst MySQL 9.6 on127.0.0.1:3306: INSERT/UPDATE/DELETEtsx, DB state matchesRemaining for #489 acceptance
applyMixins(MySqlSelectBase, [QueryPromise])runtime prototype-copy pattern. Perry's static class table doesn't model methods added at module init viaObject.defineProperty(baseClass.prototype, name, ...), soawait db.select().from(users)doesn't recognize the mixed-inthenand 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 forcaching_sha2_passwordfirst-time auth on non-TLS connections. The integration test side-steps this by warming the MySQL server's auth cache via themysqlCLI before running perry, plusPERRY_ALLOW_UNIMPLEMENTED=1at compile time. Real fix is its own larger piece.Test plan
lint,cargo-test,parity,compile-smoke,api-docs-drift,security-audit.caching_sha2_passworduser; pre-warm cache with themysqlCLI; compile withPERRY_ALLOW_UNIMPLEMENTED=1).mainare unchanged (pre-existing, unrelated_perry_wrap_perry_fn_…__textDecoder/__PATH_ERRORlink errors).