Skip to content

fix(runtime): Object.defineProperty on Class.prototype (drizzle applyMixins) — #2159 - #2242

Merged
proggeramlug merged 1 commit into
mainfrom
fix-2159-drizzle-then-inheritance
May 28, 2026
Merged

proggeramlug merged 1 commit into
mainfrom
fix-2159-drizzle-then-inheritance

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Closes #2159 — Object.defineProperty(C.prototype, name, descriptor) is now
honored on a class-ref receiver, so the applyMixins(Target, [Source])
pattern works and drizzle-orm's await db.select().from(users) resolves
through the inherited QueryPromise.then thenable.

Class.prototype evaluates to the class-ref itself in Perry (not a
separate prototype object — see the existing class_ref_id arm in
js_object_get_own_property_descriptor). Pre-fix the defineProperty
runtime helper hit extract_obj_ptr → null on that class-ref and silently
dropped the descriptor, so (new C()).method came back undefined.

The fix detects the class-ref receiver up front and installs the
descriptor's value where instance lookup will find it:

  • BOUND_METHOD_FUNC_PTR closures (what getOwnPropertyDescriptor
    returns for class methods, hot path for applyMixins): extract the
    source class-ref + method name from the closure's captures, look the
    method up in the source class's vtable chain via the existing
    lookup_class_method_in_chain, and copy the raw (func_ptr, param_count) entry onto the target class's CLASS_VTABLE_REGISTRY.
    Dispatching the bound closure directly would route through
    js_native_call_method on the source class-ref (static-dispatch arm)
    and never reach the inherited instance method.
  • Other callables (user-supplied { value: function (…) { … } }):
    route through js_register_prototype_method — the same
    Class.prototype.m = fn direct-assignment side table that
    lookup_prototype_method already consults on the instance-lookup miss
    path.

Test plan

  • New gap test test_gap_2159_defineproperty_class_prototype.ts
    pins the single-method install + full applyMixins shape + the
    await thenable assimilation. Byte-identical with
    node --experimental-strip-types.
  • tests/release/packages/drizzle-mysql/entry.ts switched from the
    explicit-.execute() workaround to idiomatic
    await db.select().from(users) form. Fixture passes (with MySQL
    running on 127.0.0.1; CI wiring is compat: Drizzle + MySQL end-to-end CI job #804).
  • cargo test --release -p perry-runtime --lib — 735/735 pass.
  • cargo fmt --all -- --check clean.

Related

…the class-vtable / prototype-method side tables (#2159)

`Class.prototype` evaluates back to the class-ref itself in Perry (not a
separate prototype object), so `Object.defineProperty(C.prototype, name,
descriptor)` hit `extract_obj_ptr → null` in `js_object_define_property`
and silently dropped the descriptor. Instance lookups (`new C().method`)
never saw the entry — drizzle-orm's `applyMixins(MySqlSelectBase,
[QueryPromise])` is the load-bearing case: it copies `then`/`catch`/
`finally` from `QueryPromise.prototype` onto `MySqlSelectBase.prototype`,
so `await db.select().from(users)` resolved to the builder itself
instead of the query result.

Fix: detect class-ref receivers up front (using the same `class_ref_id`
helper that `js_object_get_own_property_descriptor` /
`js_object_get_own_property_names` already consult on class-refs) and
install the descriptor's value where instance lookup will find it:

  * `BOUND_METHOD_FUNC_PTR` closures returned by
    `getOwnPropertyDescriptor` on a sibling class — extract the source
    class-ref + method name from the closure captures, look the method
    up in the source class's vtable chain, and copy the entry onto the
    target class's vtable. Dispatching the bound closure directly would
    re-enter `js_native_call_method` against the class-ref (static
    dispatch arm), so we copy the raw `(func_ptr, param_count)` instead;
    the regular vtable walk then invokes the method with
    `this = receiver`.
  * Any other callable — route through
    `js_register_prototype_method`, mirroring the existing
    `Class.prototype.method = fn` direct-assignment path
    (`lookup_prototype_method` is already consulted on the lookup miss).

The drizzle-mysql fixture (#489 acceptance) now uses idiomatic
`await db.select().from(users)` form instead of the `.execute()`
workaround; byte-identical with Node. New gap test
`test_gap_2159_defineproperty_class_prototype.ts` pins both the
single-method install and the full `applyMixins` shape, including
`await thenable` assimilation.
@proggeramlug
proggeramlug merged commit b0c031a into main May 28, 2026
11 checks passed
@proggeramlug
proggeramlug deleted the fix-2159-drizzle-then-inheritance branch May 28, 2026 15:22
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.

drizzle MySqlSelectBase: inherited .then resolves to undefined under Perry (await returns the builder)

1 participant