Skip to content

Commit 44b395b

Browse files
committed
feat: snapshot history/tagging and first take on snapshot tagging
The restore process will effectively be limiting factor there and in future should be replaced by pluggable system of template database (or providers supporting it).
1 parent 0555279 commit 44b395b

11 files changed

Lines changed: 1313 additions & 18 deletions

File tree

cmd/check_baselines.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"sort"
7+
8+
"github.com/boringsql/regresql/regresql"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var (
13+
checkBaselinesCwd string
14+
15+
checkBaselinesCmd = &cobra.Command{
16+
Use: "check-baselines",
17+
Short: "Check baseline-to-snapshot correlation",
18+
Long: `Check which snapshot version each baseline was created against.
19+
20+
This command shows which baselines were created with which snapshot versions,
21+
helping identify baselines that may be out of sync with the current snapshot.
22+
23+
Examples:
24+
regresql check-baselines
25+
regresql check-baselines -C /path/to/project`,
26+
Run: func(cmd *cobra.Command, args []string) {
27+
if err := checkDirectory(checkBaselinesCwd); err != nil {
28+
fmt.Print(err.Error())
29+
os.Exit(1)
30+
}
31+
if err := runCheckBaselines(); err != nil {
32+
fmt.Printf("Error: %s\n", err.Error())
33+
os.Exit(1)
34+
}
35+
},
36+
}
37+
)
38+
39+
func init() {
40+
RootCmd.AddCommand(checkBaselinesCmd)
41+
42+
checkBaselinesCmd.Flags().StringVarP(&checkBaselinesCwd, "cwd", "C", ".", "Change to directory")
43+
}
44+
45+
func runCheckBaselines() error {
46+
expectedDir := regresql.GetExpectedDir(checkBaselinesCwd)
47+
meta, err := regresql.LoadBaselineMetadata(expectedDir)
48+
if err != nil {
49+
return fmt.Errorf("failed to load baseline metadata: %w", err)
50+
}
51+
52+
if len(meta.Baselines) == 0 {
53+
fmt.Println("No baseline metadata found.")
54+
fmt.Println("\nBaseline metadata is recorded when running 'regresql update'.")
55+
fmt.Println("Run 'regresql update' to record metadata for existing baselines.")
56+
return nil
57+
}
58+
59+
snapshotsDir := regresql.GetSnapshotsDir(checkBaselinesCwd)
60+
snapshotMeta, err := regresql.ReadSnapshotMetadata(snapshotsDir)
61+
if err != nil {
62+
fmt.Println("No snapshot metadata found.")
63+
snapshotMeta = nil
64+
}
65+
66+
var currentSnapshot *regresql.SnapshotInfo
67+
if snapshotMeta != nil {
68+
currentSnapshot = snapshotMeta.Current
69+
}
70+
71+
groups := regresql.GroupBaselinesBySnapshot(meta)
72+
73+
var sortedKeys []string
74+
for key := range groups {
75+
sortedKeys = append(sortedKeys, key)
76+
}
77+
sort.Strings(sortedKeys)
78+
79+
fmt.Println("Baselines by snapshot version:")
80+
fmt.Println()
81+
82+
for _, key := range sortedKeys {
83+
baselines := groups[key]
84+
sort.Strings(baselines)
85+
86+
suffix := ""
87+
if currentSnapshot != nil {
88+
for _, path := range baselines {
89+
if info := meta.Baselines[path]; info != nil && info.SnapshotHash == currentSnapshot.Hash {
90+
suffix = " [current]"
91+
break
92+
}
93+
}
94+
}
95+
96+
fmt.Printf(" %s%s:\n", key, suffix)
97+
for _, baseline := range baselines {
98+
fmt.Printf(" - %s\n", baseline)
99+
}
100+
fmt.Println()
101+
}
102+
103+
if currentSnapshot != nil {
104+
matched, outdated := regresql.CheckBaselineCorrelation(meta, currentSnapshot)
105+
106+
if len(outdated) > 0 {
107+
fmt.Printf("WARNING: %d baseline(s) created with outdated snapshots.\n", len(outdated))
108+
fmt.Println("These may fail tests against the current snapshot.")
109+
fmt.Println()
110+
fmt.Println("Outdated baselines:")
111+
sort.Strings(outdated)
112+
for _, path := range outdated {
113+
info := meta.Baselines[path]
114+
tag := info.SnapshotTag
115+
if tag == "" {
116+
tag = regresql.TruncateHash(info.SnapshotHash)
117+
}
118+
fmt.Printf(" - %s (created with %s)\n", path, tag)
119+
}
120+
fmt.Println()
121+
fmt.Println("To synchronize:")
122+
fmt.Println(" regresql update")
123+
fmt.Println()
124+
} else if len(matched) > 0 {
125+
fmt.Printf("All %d baseline(s) are synchronized with the current snapshot.\n", len(matched))
126+
}
127+
} else {
128+
fmt.Println("Note: No current snapshot found. Cannot check for outdated baselines.")
129+
}
130+
131+
return nil
132+
}

cmd/diff.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/boringsql/regresql/regresql"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var (
12+
diffCwd string
13+
diffFrom string
14+
diffTo string
15+
diffQuery string
16+
diffRunFilter string
17+
18+
diffCmd = &cobra.Command{
19+
Use: "diff",
20+
Short: "Compare query outputs between two snapshot versions",
21+
Long: `Compare query outputs between two snapshot versions.
22+
23+
This command restores two different snapshots and runs all queries against both,
24+
then shows the differences in query output.
25+
26+
Examples:
27+
regresql diff --from v1 --to v2
28+
regresql diff --from v1 --to v3 --query orders/get_order_total.sql
29+
regresql diff --from sha256:abc123 --to current`,
30+
Run: func(cmd *cobra.Command, args []string) {
31+
if err := checkDirectory(diffCwd); err != nil {
32+
fmt.Print(err.Error())
33+
os.Exit(1)
34+
}
35+
if err := runDiff(); err != nil {
36+
fmt.Printf("Error: %s\n", err.Error())
37+
os.Exit(1)
38+
}
39+
},
40+
}
41+
)
42+
43+
func init() {
44+
RootCmd.AddCommand(diffCmd)
45+
46+
diffCmd.Flags().StringVarP(&diffCwd, "cwd", "C", ".", "Change to directory")
47+
diffCmd.Flags().StringVar(&diffFrom, "from", "", "Source snapshot (tag or hash prefix)")
48+
diffCmd.Flags().StringVar(&diffTo, "to", "", "Target snapshot (tag, hash, or 'current', default: current)")
49+
diffCmd.Flags().StringVar(&diffQuery, "query", "", "Specific query to compare (optional)")
50+
diffCmd.Flags().StringVar(&diffRunFilter, "run", "", "Run only queries matching regexp")
51+
52+
diffCmd.MarkFlagRequired("from")
53+
}
54+
55+
func runDiff() error {
56+
snapshotsDir := regresql.GetSnapshotsDir(diffCwd)
57+
58+
metadata, err := regresql.ReadSnapshotMetadata(snapshotsDir)
59+
if err != nil {
60+
return fmt.Errorf("no snapshot metadata found: %w", err)
61+
}
62+
63+
fromInfo, err := regresql.ResolveSnapshot(metadata, diffFrom)
64+
if err != nil {
65+
return fmt.Errorf("cannot resolve --from snapshot: %w", err)
66+
}
67+
68+
toRef := diffTo
69+
if toRef == "" || toRef == "current" {
70+
if metadata.Current == nil {
71+
return fmt.Errorf("no current snapshot")
72+
}
73+
toRef = metadata.Current.Hash
74+
}
75+
toInfo, err := regresql.ResolveSnapshot(metadata, toRef)
76+
if err != nil {
77+
return fmt.Errorf("cannot resolve --to snapshot: %w", err)
78+
}
79+
80+
if !regresql.SnapshotExists(fromInfo) {
81+
return fmt.Errorf("source snapshot file not found: %s", fromInfo.Path)
82+
}
83+
if !regresql.SnapshotExists(toInfo) {
84+
return fmt.Errorf("target snapshot file not found: %s", toInfo.Path)
85+
}
86+
87+
if fromInfo.Hash == toInfo.Hash {
88+
fmt.Printf("Both snapshots are identical (%s)\n", regresql.FormatSnapshotRef(fromInfo))
89+
return nil
90+
}
91+
92+
fmt.Printf("Comparing snapshots:\n")
93+
fmt.Printf(" From: %s (%s)\n", regresql.FormatSnapshotRef(fromInfo), fromInfo.Path)
94+
fmt.Printf(" To: %s (%s)\n", regresql.FormatSnapshotRef(toInfo), toInfo.Path)
95+
fmt.Println()
96+
97+
result, err := regresql.DiffSnapshots(diffCwd, fromInfo, toInfo, diffQuery, diffRunFilter)
98+
if err != nil {
99+
return err
100+
}
101+
102+
printDiffResult(result, fromInfo, toInfo)
103+
104+
return nil
105+
}
106+
107+
func printDiffResult(result *regresql.SnapshotDiffResult, from, to *regresql.SnapshotInfo) {
108+
if len(result.Changed) == 0 && len(result.Errors) == 0 {
109+
fmt.Printf("No differences found (%d queries compared)\n", len(result.Unchanged))
110+
return
111+
}
112+
113+
if len(result.Changed) > 0 {
114+
fmt.Printf("CHANGED (%d):\n", len(result.Changed))
115+
for _, diff := range result.Changed {
116+
fmt.Printf(" %s\n", diff.QueryPath)
117+
if diff.FromRows != diff.ToRows {
118+
fmt.Printf(" Rows: %d → %d\n", diff.FromRows, diff.ToRows)
119+
}
120+
if diff.Diff != "" {
121+
// Show first few lines of diff
122+
lines := splitLines(diff.Diff, 5)
123+
for _, line := range lines {
124+
fmt.Printf(" %s\n", line)
125+
}
126+
if len(lines) == 5 {
127+
fmt.Printf(" ...\n")
128+
}
129+
}
130+
}
131+
fmt.Println()
132+
}
133+
134+
if len(result.Errors) > 0 {
135+
fmt.Printf("ERRORS (%d):\n", len(result.Errors))
136+
for _, e := range result.Errors {
137+
fmt.Printf(" %s: %s\n", e.QueryPath, e.Error)
138+
}
139+
fmt.Println()
140+
}
141+
142+
fmt.Printf("SUMMARY:\n")
143+
fmt.Printf(" Changed: %d\n", len(result.Changed))
144+
fmt.Printf(" Unchanged: %d\n", len(result.Unchanged))
145+
if len(result.Errors) > 0 {
146+
fmt.Printf(" Errors: %d\n", len(result.Errors))
147+
}
148+
}
149+
150+
func splitLines(s string, max int) []string {
151+
var lines []string
152+
start := 0
153+
count := 0
154+
for i := 0; i < len(s) && count < max; i++ {
155+
if s[i] == '\n' {
156+
if i > start {
157+
lines = append(lines, s[start:i])
158+
count++
159+
}
160+
start = i + 1
161+
}
162+
}
163+
if start < len(s) && count < max {
164+
lines = append(lines, s[start:])
165+
}
166+
return lines
167+
}

0 commit comments

Comments
 (0)