Skip to content

Generated record equals() compares value-wrapper fields by reference (Decimal, Temporal, etc.) #202

Description

@danfma

Background

C# `record` types lower to TS classes whose `equals(other)` method compares every field with strict equality (`this.x === other.x`). For primitive fields this matches C# semantics; for value-wrapper types like `decimal.js` `Decimal` (the BCL mapping for C# `decimal`) and Temporal types it does not — two numerically equal `Decimal` instances are different objects, so `equals` returns `false` even when the records would compare equal in C#.

Surfaced on `SampleQueryableSqlite.Product` (#201) — `Product.UnitPrice` is a `decimal` field; the generated `equals` compares the two `Decimal` instances by reference and silently diverges from .NET semantics. The same wrapper hits Temporal types (`PlainDate`, `PlainDateTime`, `Duration`) once those flow through a record.

Proposal

For each record field, the bridge should pick the comparison strategy from the field's lowered type:

  • Primitive (number / string / bigint / boolean): `===`.
  • Value-wrapper with a known `equals`-shaped method (Decimal, Temporal, HashSet/HashMap, our own `[Branded]`/`[InlineWrapper]` types): call `x.equals(y)` (or the type-specific helper, e.g. `Decimal.eq(a, b)`, `Temporal.PlainDate.compare(a, b) === 0`).
  • Reference type: keep `===` (current behavior — matches reference-equality for nullable record fields).

The bridge already inspects field types at lowering time (`IrToTsExpressionBridge` / record handler); the comparison choice can fold into the same dispatch.

Scope

  • Identify the comparison helper per BCL mapping (Decimal: `.eq()`, Temporal: `.equals()` / `.compare() === 0`, HashSet / HashMap: structural).
  • Update the record `equals` lowering to pick the right helper per field type.
  • Update `hashCode` accordingly so the contract stays consistent — use the wrapper's stable hash instead of the wrapper's identity.
  • Tests for Decimal / Temporal record fields.

Workaround until this lands

Override `equals` / `GetHashCode` manually on records carrying value-wrapper fields. For DTOs that flow through JSON, the issue is moot because the wire shape is plain — only in-memory comparisons hit the bug.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions