From 6c6fb5d3d2f8bfef5c5bac30f370646a6ff5f73a Mon Sep 17 00:00:00 2001 From: Tianzhou Date: Sat, 21 Feb 2026 19:38:27 -0800 Subject: [PATCH] fix: exclude DiffSource interface field from JSON serialization (#305) Plans generated with --debug included the Diff.Source field (a DiffSource interface) in JSON output. Go's json.Unmarshal cannot reconstruct interface types, causing FromJSON() to fail when applying debug plans. Change the JSON tag from `json:"source,omitempty"` to `json:"-"` to exclude Source from serialization. All in-memory usage (rewrite, formatting, display) is unaffected. Co-Authored-By: Claude Opus 4.6 --- internal/diff/diff.go | 2 +- internal/plan/plan_test.go | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/internal/diff/diff.go b/internal/diff/diff.go index aad14c69..bb872b79 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -251,7 +251,7 @@ type Diff struct { Type DiffType `json:"type"` Operation DiffOperation `json:"operation"` // create, alter, drop, replace Path string `json:"path"` - Source DiffSource `json:"source,omitempty"` + Source DiffSource `json:"-"` // interface; not JSON-serializable (see #305) } type ddlDiff struct { diff --git a/internal/plan/plan_test.go b/internal/plan/plan_test.go index 7b70161f..76803ea5 100644 --- a/internal/plan/plan_test.go +++ b/internal/plan/plan_test.go @@ -260,3 +260,64 @@ func TestPlanJSONLoadedSummary(t *testing.T) { t.Error("Summary should not say \"No changes detected\" when there are changes") } } + +func TestPlanDebugJSONRoundTrip(t *testing.T) { + // Issue #305: Plans generated with --debug produce JSON that cannot be + // deserialized by FromJSON() because the Diff.Source field is a Go interface + // (DiffSource) that json.Unmarshal cannot reconstruct. + oldSQL := `CREATE TABLE users ( + id integer NOT NULL + );` + + newSQL := `CREATE TABLE users ( + id integer NOT NULL, + name text NOT NULL + ); + CREATE TABLE posts ( + id integer NOT NULL, + title text NOT NULL + );` + + oldIR := parseSQL(t, oldSQL) + newIR := parseSQL(t, newSQL) + diffs := diff.GenerateMigration(oldIR, newIR, "public") + + p := NewPlan(diffs) + + // Serialize with debug mode (includes SourceDiffs; Diff.Source is excluded via json:"-") + debugJSON, err := p.ToJSONWithDebug(true) + if err != nil { + t.Fatalf("Failed to serialize plan with debug: %v", err) + } + + // Deserialize - this should succeed + loaded, err := FromJSON([]byte(debugJSON)) + if err != nil { + t.Fatalf("Failed to deserialize debug plan JSON: %v", err) + } + + // Verify debug mode actually included SourceDiffs + if len(loaded.SourceDiffs) == 0 { + t.Error("Debug plan should include SourceDiffs") + } + + // Verify the loaded plan has valid groups and steps + if len(loaded.Groups) == 0 { + t.Error("Loaded plan should have at least one execution group") + } + + // Re-serialize without debug and verify round-trip stability + normalJSON, err := loaded.ToJSON() + if err != nil { + t.Fatalf("Failed to re-serialize loaded plan: %v", err) + } + + loaded2, err := FromJSON([]byte(normalJSON)) + if err != nil { + t.Fatalf("Failed to deserialize re-serialized plan: %v", err) + } + + if len(loaded2.Groups) != len(loaded.Groups) { + t.Errorf("Group count mismatch: got %d, want %d", len(loaded2.Groups), len(loaded.Groups)) + } +}