Skip to content

Cross-module instanceof returns true for unrelated imported classes #585

Closed

Description

@proggeramlug

Repro

// lib_a.ts
export class A { ax = 1 }
export class B { bx = 2 }

// main.ts
import { A, B } from './lib_a.js';
const a = new A();
console.log(a instanceof A);  // true (correct)
console.log(a instanceof B);  // false (correct in this minimal case)

That minimal case actually works. But the bug surfaces when the value is an Object.assign'd instance of a class imported from one module, checked against an unrelated class imported from a different module:

// surfaces while compiling drizzle-orm 0.45.2 via perry.compilePackages
function pgTableWithSchema(name) {
  const rawTable = new PgTable(name);          // PgTable lives in pg-core/table.js
  return Object.assign(rawTable, { /* cols */ });
}
const users = pgTableWithSchema('users');

// drizzle-orm/entity.js's `is()` brand check
import { Subquery } from '../../subquery.js';   // unrelated class, separate module
console.log(users instanceof Subquery);          // perry: TRUE (WRONG); node/bun: FALSE

In the actual drizzle run via perry, all three of these return true:

  • users instanceof Subquery
  • users instanceof PgViewBase
  • users instanceof SQL

Why it matters for #488

drizzle-orm's is(value, kind) brand-check (in entity.js) short-circuits with if (value instanceof type) return true;. With this bug, is(table, Subquery) returns true unconditionally. drizzle's from() builder then takes the Subquery branch, reads table._.selectedFields, and crashes with TypeError: Cannot read properties of undefined (reading 'selectedFields').

This is the dominant query-side blocker for #488 (drizzle + @perryts/postgres acceptance test).

Where I'd start digging

crates/perry-codegen/src/expr.rs:5498 Expr::InstanceOf — for the cross-module case, falls back to cid = 0 when ctx.class_ids.get(ty) misses (line 5526-5539). Then js_instanceof(value, 0) is called.

crates/perry-runtime/src/object.rs:3806 js_instanceof — at line 4047-4050:

let obj_class_id = (*obj_ptr).class_id;
if obj_class_id == class_id { return true_val; }

If both obj_class_id and the requested class_id happen to be 0 (e.g. an Object.assign'd table whose class_id metadata was lost + an unresolved cross-module class name), 0 == 0 returns true.

Probable fix surfaces:

  1. Codegen: ensure cross-module Subquery/PgViewBase/SQL resolve to a non-zero class id (likely working — see imported_class_stubs registration in codegen.rs:462). Verify they're being threaded through correctly when compilePackages is used vs. ordinary .ts imports.
  2. Runtime: never return true for class_id == 0 — that should be an "unknown class" sentinel that always returns false.
  3. Codegen: Object.assign(rawTable, { ... }) may lose the rawTable's class_id; verify the result still has the correct ObjectHeader.class_id.

Workaround

Patch drizzle's entity.js to skip the instanceof short-circuit and rely on the entityKind chain walk:

// if (value instanceof type) return true;  // skip on perry

With this patch the chain walk also fails (A[sym] returns undefined for static Symbol-keyed fields — separate issue), but the path advances to the next blocker (#TBD: thenables). So is() is the gate.

Refs

Blocks #488. Surfaced from #420 sub-issue acceptance run on v0.5.684.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions