Skip to content

Object.assign(target, src) drops Symbol-keyed properties already on target #590

Description

@proggeramlug

Repro

const SymCols = Symbol.for('test:cols');

class TBase {
  static Symbol = { Columns: SymCols };
  [SymCols]: any;
}

const t: any = new TBase();
t[TBase.Symbol.Columns] = { id: { name: 'id' }, name: { name: 'name' } };
console.log('before assign:', t[TBase.Symbol.Columns]);

Object.assign(t, { extraField: 1 });
console.log('after assign:', t[TBase.Symbol.Columns]);

Bun output (correct):

before assign: { id: { name: 'id' }, name: { name: 'name' } }
after assign:  { id: { name: 'id' }, name: { name: 'name' } }

Perry output (bug):

before assign: { id: { name: 'id' }, name: { name: 'name' } }
after assign:  undefined

Spec

Object.assign(target, ...sources) only enumerates own enumerable string-keyed and Symbol-keyed properties on each source and assigns them to target. Properties already on target (string-keyed or Symbol-keyed) are preserved unless overwritten by a source. Perry retains string-keyed target properties through the assign but drops Symbol-keyed ones.

Why it matters for #488

drizzle-orm's pgTableWithSchema assembles a table instance like this (pg-core/table.js:42-53):

const rawTable = new PgTable(name, schema, baseName);
// ... build columns ...
const table = Object.assign(rawTable, builtColumns);   // <-- Object.assign #1
table[Table.Symbol.Columns] = builtColumns;             // <-- Symbol-keyed write
table[Table.Symbol.ExtraConfigColumns] = builtColumnsForExtraConfig;
return Object.assign(table, { enableRLS: ... });        // <-- Object.assign #2 drops the Symbols

By the time dialect.buildInsertQuery({ table }) runs (pg-core/dialect.js:358), table[Table.Symbol.Columns] reads as undefined, and the next line (Object.entries(undefined).filter(...)) SIGSEGVs in js_object_entries reading 0x11 as a pointer.

The drizzle case has the writes BEFORE the second Object.assign — even cleaner: any Symbol property the writer set is wiped by the trailing Object.assign(table, { enableRLS }). The repro above mirrors the same shape.

Where I'd start digging

crates/perry-runtime/src/object.rsjs_object_assign / wherever Object.assign target preservation lives. The likely shape is that the implementation copies own string-keyed properties from target to a fresh object, copies sources, and returns the new object — without iterating the target's Symbol-keyed side table (SYMBOL_PROPERTIES or whatever it's called in v0.5.622's groundwork).

Side benefit of fixing this: the Object.assign-loses-class-id bug surfaced earlier in the session (Object.assign'd instance returning false for instanceof PgTable) might be fixable in the same pass — preserve the target's GC class_id and SYMBOL_PROPERTIES side-table when constructing the result.

Workaround

User-side: assign Symbol-keyed properties AFTER all Object.assign calls. drizzle's source order makes this hard to monkey-patch from outside the package; cleanest fix is on perry's side.

Refs

Blocks #488 (drizzle + @perryts/postgres acceptance) along with #588.

After this lands, the next blocker on #488 is whatever fires after dialect.buildInsertQuery succeeds — haven't reached that frame yet.

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