Skip to content

Bug: DiffSource interface field on Diff breaks JSON deserialization in debug mode #305

@ryespresso

Description

@ryespresso

Bug: DiffSource interface field on Diff breaks JSON deserialization in debug mode

Note: Excuse the AI written bug report. Was having some issues apply a plan in debug mode and had claude check out what could be the possible culprit. Feel free to close if this is not the case.

Summary

Plans generated with --debug produce JSON that cannot be deserialized by FromJSON(). The Diff.Source field is typed as a Go interface (DiffSource), which Go's json.Unmarshal cannot reconstruct from JSON.

Steps to Reproduce

  1. Create a schema change that produces diffs (e.g., alter a materialized view)
  2. Generate a plan with debug mode: pgschema plan --debug -o json:plan.json ...
  3. Attempt to apply it: pgschema apply --plan plan.json ...
  4. Apply fails with a JSON unmarshal error

Root Cause

In internal/diff/diff.go (line 254):

type DiffSource interface {
    GetObjectName() string
}

type Diff struct {
    Statements []SQLStatement `json:"statements"`
    Type       DiffType       `json:"type"`
    Operation  DiffOperation  `json:"operation"`
    Path       string         `json:"path"`
    Source     DiffSource     `json:"source,omitempty"`  // <-- interface field
}

The Source field holds concrete types like *viewDiff, *tableDiff, *ir.Table, etc. — all implementing DiffSource.

Marshaling works: Go's json.Marshal uses reflection to serialize the concrete type, producing nested JSON objects with Old, New, and other fields.

Unmarshaling fails: Go's json.Unmarshal encounters a JSON object for the source key and attempts to produce a map[string]interface{}, which does not implement DiffSource (no GetObjectName() method). The decoder fails.

This was introduced in commit a574dccf ("chore: make source strong type"), which changed Source any to Source DiffSource.

Impact

  • Normal mode: Not affected. ToJSON() calls ToJSONWithDebug(false), which nils out SourceDiffs before marshaling. The source field never appears in the output JSON.
  • Debug mode: Affected. ToJSONWithDebug(true) keeps SourceDiffs, and each diff's Source gets marshaled as a nested object that cannot be deserialized.

Suggested Fix

Change the JSON tag on Source from json:"source,omitempty" to json:"-":

Source     DiffSource     `json:"-"`

This preserves full type safety for all in-memory usage (rewrite logic in plan/rewrite.go, summary display in plan/plan.go, dump formatting in dump/formatter.go). Only JSON serialization is affected — Source is excluded from both marshal and unmarshal, which is correct since no code path reads Source from deserialized JSON.

Relevant Files

File Role
internal/diff/diff.go:254 Diff.Source field definition
internal/plan/plan.go:67 SourceDiffs []diff.Diff on Plan struct
internal/plan/plan.go:354-365 ToJSONWithDebug — controls whether SourceDiffs is included
internal/plan/plan.go:380-387 FromJSON — deserialization entry point
cmd/apply/apply.go:292 Where FromJSON is called during apply

Environment

  • pgschema version: v1.7.2 (bug present since commit a574dccf, which predates v1.7.1)
  • Go version: 1.24+

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions