|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + |
| 7 | + "github.com/spf13/cobra" |
| 8 | + |
| 9 | + "github.com/boringsql/dryrun/internal/history" |
| 10 | + "github.com/boringsql/dryrun/pkg/diff" |
| 11 | +) |
| 12 | + |
| 13 | +// default is store-to-store, no DB connection. --live is the one exception |
| 14 | +// (schema only) and never captures behind the user's back. |
| 15 | +func newDiffCmd(historyDB *string) *cobra.Command { |
| 16 | + var ( |
| 17 | + fromHash, toHash string |
| 18 | + kindFlag, nodeFlag string |
| 19 | + latest, live bool |
| 20 | + prettyDiff, jsonDiff bool |
| 21 | + minPct float64 |
| 22 | + ) |
| 23 | + |
| 24 | + c := &cobra.Command{ |
| 25 | + Use: "diff [<from> <to>]", |
| 26 | + Short: "Diff two snapshots of the same kind", |
| 27 | + Long: `Diff two snapshots of the same kind, resolved from history.db (no connection). |
| 28 | +
|
| 29 | + dryrun snapshot diff <from> <to> diff two snapshots by hash prefix |
| 30 | + dryrun snapshot diff --latest diff the previous capture against the latest |
| 31 | + dryrun snapshot diff latest~1 latest --kind planner |
| 32 | + same, for a specific kind |
| 33 | + dryrun snapshot diff <from> --live diff a stored snapshot against the database now (schema only) |
| 34 | +
|
| 35 | +latest / latest~N name a snapshot of the kind given by --kind (default schema); |
| 36 | +hash-prefix operands carry their own kind. Mixing kinds is rejected.`, |
| 37 | + Args: cobra.MaximumNArgs(2), |
| 38 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 39 | + store, err := openHistoryStore(*historyDB) |
| 40 | + if err != nil { |
| 41 | + return err |
| 42 | + } |
| 43 | + defer store.Close() |
| 44 | + |
| 45 | + ctx := cmd.Context() |
| 46 | + key := resolveSnapshotKey() |
| 47 | + |
| 48 | + fromTok, toTok, liveTo, err := diffOperands(args, fromHash, toHash, latest, live) |
| 49 | + if err != nil { |
| 50 | + return err |
| 51 | + } |
| 52 | + |
| 53 | + fromKind, fromRef, err := store.ResolveToken(ctx, key, fromTok, kindFlag, nodeFlag) |
| 54 | + if err != nil { |
| 55 | + return err |
| 56 | + } |
| 57 | + fromSnap, err := store.Get(ctx, key, fromKind, fromRef) |
| 58 | + if err != nil { |
| 59 | + return err |
| 60 | + } |
| 61 | + |
| 62 | + if liveTo { |
| 63 | + if fromKind.Tag != history.KindSchema { |
| 64 | + return fmt.Errorf("--live is schema-only (it reads the catalog); %s is a %s snapshot\n"+ |
| 65 | + " take a snapshot first and diff store-to-store", |
| 66 | + short(fromTok), fromKind) |
| 67 | + } |
| 68 | + lctx, conn, cerr := connectDB() |
| 69 | + if cerr != nil { |
| 70 | + return cerr |
| 71 | + } |
| 72 | + defer conn.Close() |
| 73 | + live, lerr := conn.Introspect(lctx) |
| 74 | + if lerr != nil { |
| 75 | + return lerr |
| 76 | + } |
| 77 | + env, berr := buildSnapshotDiff(fromKind, fromSnap, history.WrapSchema(live)) |
| 78 | + if berr != nil { |
| 79 | + return berr |
| 80 | + } |
| 81 | + return emitDiff(env, jsonDiff, prettyDiff, minPct) |
| 82 | + } |
| 83 | + |
| 84 | + toKind, toRef, err := store.ResolveToken(ctx, key, toTok, kindFlag, nodeFlag) |
| 85 | + if err != nil { |
| 86 | + return err |
| 87 | + } |
| 88 | + if fromKind.Tag != toKind.Tag { |
| 89 | + return fmt.Errorf("not comparable: %s is a %s snapshot, %s is a %s snapshot\n"+ |
| 90 | + " diff snapshots of the same kind (schema↔schema, planner↔planner, activity↔activity)", |
| 91 | + short(fromTok), fromKind, short(toTok), toKind) |
| 92 | + } |
| 93 | + toSnap, err := store.Get(ctx, key, toKind, toRef) |
| 94 | + if err != nil { |
| 95 | + return err |
| 96 | + } |
| 97 | + |
| 98 | + env, err := buildSnapshotDiff(fromKind, fromSnap, toSnap) |
| 99 | + if err != nil { |
| 100 | + return err |
| 101 | + } |
| 102 | + return emitDiff(env, jsonDiff, prettyDiff, minPct) |
| 103 | + }, |
| 104 | + } |
| 105 | + |
| 106 | + c.Flags().StringVar(&fromHash, "from", "", "source snapshot (compat; prefer positional)") |
| 107 | + c.Flags().StringVar(&toHash, "to", "", "target snapshot (compat; prefer positional)") |
| 108 | + c.Flags().BoolVar(&latest, "latest", false, "diff the previous capture against the latest (latest~1..latest)") |
| 109 | + c.Flags().StringVar(&kindFlag, "kind", "schema", "kind for latest/latest~N operands: schema|planner|activity") |
| 110 | + c.Flags().StringVar(&nodeFlag, "node", "", "activity node label (when --kind activity has multiple nodes)") |
| 111 | + c.Flags().BoolVar(&live, "live", false, "diff a stored snapshot against the live database (schema only)") |
| 112 | + c.Flags().BoolVar(&jsonDiff, "json", false, "output the SnapshotDiff as JSON") |
| 113 | + c.Flags().BoolVar(&prettyDiff, "pretty", false, "pretty-print JSON") |
| 114 | + c.Flags().Float64Var(&minPct, "min-pct", diff.DefaultMinPct, "console: hide planner/activity rows whose |Δ| is below this percent") |
| 115 | + return c |
| 116 | +} |
| 117 | + |
| 118 | +// store-to-store needs two operands; --live needs exactly one. |
| 119 | +func diffOperands(args []string, fromHash, toHash string, latest, live bool) (from, to string, liveTo bool, err error) { |
| 120 | + var ops []string |
| 121 | + if len(args) > 0 { |
| 122 | + ops = append(ops, args...) |
| 123 | + } else { |
| 124 | + if fromHash != "" { |
| 125 | + ops = append(ops, fromHash) |
| 126 | + } |
| 127 | + if toHash != "" { |
| 128 | + ops = append(ops, toHash) |
| 129 | + } |
| 130 | + if latest { |
| 131 | + if len(ops) == 0 { |
| 132 | + ops = []string{"latest~1", "latest"} |
| 133 | + } else { |
| 134 | + ops = append(ops, "latest") |
| 135 | + } |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + if live { |
| 140 | + if len(ops) != 1 { |
| 141 | + return "", "", false, fmt.Errorf("--live diffs one stored snapshot against the database now; give exactly one snapshot") |
| 142 | + } |
| 143 | + return ops[0], "", true, nil |
| 144 | + } |
| 145 | + |
| 146 | + // legacy: bare `--from <hash>` (no positional, no --to) meant diff against live |
| 147 | + if len(args) == 0 && fromHash != "" && toHash == "" && !latest { |
| 148 | + return fromHash, "", true, nil |
| 149 | + } |
| 150 | + |
| 151 | + if len(ops) != 2 { |
| 152 | + return "", "", false, fmt.Errorf("specify two snapshots: `diff <from> <to>`, `--latest`, or `--from/--to` (add `--live` to diff against the database)") |
| 153 | + } |
| 154 | + return ops[0], ops[1], false, nil |
| 155 | +} |
| 156 | + |
| 157 | +func buildSnapshotDiff(kind history.SnapshotKind, from, to history.StoredSnapshot) (*diff.SnapshotDiff, error) { |
| 158 | + env := &diff.SnapshotDiff{ |
| 159 | + FromHash: from.ContentHash(), |
| 160 | + ToHash: to.ContentHash(), |
| 161 | + FromTakenAt: from.Timestamp(), |
| 162 | + ToTakenAt: to.Timestamp(), |
| 163 | + } |
| 164 | + switch kind.Tag { |
| 165 | + case history.KindSchema: |
| 166 | + d, err := diff.DiffSchema(from.AsSchema(), to.AsSchema()) |
| 167 | + if err != nil { |
| 168 | + return nil, err |
| 169 | + } |
| 170 | + env.Kind, env.Schema = "schema", d |
| 171 | + case history.KindPlanner: |
| 172 | + d, err := diff.DiffPlanner(from.AsPlanner(), to.AsPlanner()) |
| 173 | + if err != nil { |
| 174 | + return nil, err |
| 175 | + } |
| 176 | + env.Kind, env.Planner = "planner", d |
| 177 | + case history.KindActivity: |
| 178 | + d, err := diff.DiffActivity(from.AsActivity(), to.AsActivity()) |
| 179 | + if err != nil { |
| 180 | + return nil, err |
| 181 | + } |
| 182 | + env.Kind, env.Activity = "activity", d |
| 183 | + default: |
| 184 | + return nil, fmt.Errorf("unsupported diff kind %s", kind) |
| 185 | + } |
| 186 | + return env, nil |
| 187 | +} |
| 188 | + |
| 189 | +func emitDiff(env *diff.SnapshotDiff, jsonDiff, prettyDiff bool, minPct float64) error { |
| 190 | + if jsonDiff { |
| 191 | + fmt.Println(string(marshalJSON(env, prettyDiff))) |
| 192 | + return nil |
| 193 | + } |
| 194 | + diff.RenderConsoleMinPct(os.Stdout, env, minPct) |
| 195 | + return nil |
| 196 | +} |
0 commit comments