Skip to content

Commit 006b74b

Browse files
committed
feat: selecting update for better DX
1 parent d787921 commit 006b74b

6 files changed

Lines changed: 323 additions & 34 deletions

File tree

cmd/update.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,43 @@ var (
1414
updateCommit bool
1515
updateNoRestore bool
1616
updateForceRestore bool
17+
updatePending bool
18+
updateInteractive bool
19+
updateDryRun bool
1720

1821
// updateCmd represents the update command
1922
updateCmd = &cobra.Command{
20-
Use: "update [flags]",
23+
Use: "update [path...]",
2124
Short: "Creates or updates the expected output files",
25+
Long: `Creates or updates the expected output files for queries.
26+
27+
Without arguments, updates all queries. With path arguments, only updates
28+
queries matching those paths.
29+
30+
Examples:
31+
regresql update # Update all queries
32+
regresql update orders/ # Update queries in orders/
33+
regresql update orders/get_order.sql # Update specific query
34+
regresql update --pending # Only create missing baselines
35+
regresql update --dry-run # Preview what would be updated
36+
regresql update --interactive # Review each change`,
37+
Args: cobra.ArbitraryArgs,
2238
Run: func(cmd *cobra.Command, args []string) {
2339
if err := checkDirectory(updateCwd); err != nil {
2440
fmt.Print(err.Error())
2541
os.Exit(1)
2642
}
27-
regresql.Update(updateCwd, updateRunFilter, updateCommit, updateNoRestore, updateForceRestore)
43+
regresql.Update(regresql.UpdateOptions{
44+
Root: updateCwd,
45+
RunFilter: updateRunFilter,
46+
Paths: args,
47+
Commit: updateCommit,
48+
NoRestore: updateNoRestore,
49+
ForceRestore: updateForceRestore,
50+
Pending: updatePending,
51+
Interactive: updateInteractive,
52+
DryRun: updateDryRun,
53+
})
2854
},
2955
}
3056
)
@@ -37,4 +63,7 @@ func init() {
3763
updateCmd.Flags().BoolVar(&updateCommit, "commit", false, "Commit transactions instead of rollback (use with caution)")
3864
updateCmd.Flags().BoolVar(&updateNoRestore, "no-restore", false, "Skip snapshot restore before update")
3965
updateCmd.Flags().BoolVar(&updateForceRestore, "force-restore", false, "Force snapshot restore even if unchanged")
66+
updateCmd.Flags().BoolVar(&updatePending, "pending", false, "Only create baselines for queries without expected files")
67+
updateCmd.Flags().BoolVar(&updateInteractive, "interactive", false, "Review and confirm each update")
68+
updateCmd.Flags().BoolVar(&updateDryRun, "dry-run", false, "Show what would be updated without writing files")
4069
}

regresql/interactive.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package regresql
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"strings"
9+
)
10+
11+
// ErrUserQuit is returned when the user chooses to quit during interactive mode
12+
var ErrUserQuit = errors.New("user quit")
13+
14+
// InteractivePrompter handles interactive user prompts for update operations
15+
type InteractivePrompter struct {
16+
reader *bufio.Reader
17+
}
18+
19+
// NewInteractivePrompter creates a new interactive prompter
20+
func NewInteractivePrompter() *InteractivePrompter {
21+
return &InteractivePrompter{reader: bufio.NewReader(os.Stdin)}
22+
}
23+
24+
// PromptAccept shows query name and asks user to accept/skip/quit
25+
// Returns: "accept", "skip", "quit"
26+
func (p *InteractivePrompter) PromptAccept(queryName string, diff string) string {
27+
fmt.Printf("\nQuery: %s\n", queryName)
28+
if diff != "" {
29+
fmt.Printf("%s\n", diff)
30+
}
31+
fmt.Print("[a]ccept / [s]kip / [q]uit: ")
32+
33+
input, _ := p.reader.ReadString('\n')
34+
input = strings.TrimSpace(strings.ToLower(input))
35+
36+
switch input {
37+
case "a", "accept", "":
38+
return "accept"
39+
case "q", "quit":
40+
return "quit"
41+
default:
42+
return "skip"
43+
}
44+
}

regresql/plans.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,45 @@ func getResultSetPath(p *Plan, targetdir string, index int) string {
260260
}
261261
return filepath.Join(targetdir, rsFileName)
262262
}
263+
264+
// ComputeDiffForInteractive computes a diff between current results and existing expected files
265+
// Returns a human-readable diff string for interactive review
266+
func (p *Plan) ComputeDiffForInteractive(expectedDir string) string {
267+
var diffs []string
268+
269+
for i, rs := range p.ResultSets {
270+
expectedPath := getResultSetPath(p, expectedDir, i)
271+
272+
// Check if expected file exists
273+
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
274+
diffs = append(diffs, fmt.Sprintf(" [NEW] %s (%d rows)", filepath.Base(expectedPath), len(rs.Rows)))
275+
continue
276+
}
277+
278+
// Load existing expected result
279+
expected, err := LoadResultSet(expectedPath)
280+
if err != nil {
281+
diffs = append(diffs, fmt.Sprintf(" [ERROR] %s: %v", filepath.Base(expectedPath), err))
282+
continue
283+
}
284+
285+
// Compare row counts
286+
if len(expected.Rows) != len(rs.Rows) {
287+
diffs = append(diffs, fmt.Sprintf(" [CHANGED] %s: %d rows → %d rows",
288+
filepath.Base(expectedPath), len(expected.Rows), len(rs.Rows)))
289+
continue
290+
}
291+
292+
// Quick check if content differs (compare JSON representations)
293+
expectedJSON := expected.ToJSON()
294+
actualJSON := rs.ToJSON()
295+
if expectedJSON != actualJSON {
296+
diffs = append(diffs, fmt.Sprintf(" [CHANGED] %s: content differs", filepath.Base(expectedPath)))
297+
}
298+
}
299+
300+
if len(diffs) == 0 {
301+
return " (no changes)"
302+
}
303+
return strings.Join(diffs, "\n")
304+
}

regresql/regresql.go

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,34 @@ import (
66
"time"
77
)
88

9-
type TestOptions struct {
10-
Root string
11-
RunFilter string
12-
FormatName string
13-
OutputPath string
14-
Commit bool
15-
NoRestore bool
16-
ForceRestore bool
17-
FailOnSkipped bool
18-
Color bool
19-
NoColor bool
20-
FullDiff bool
21-
NoDiff bool
22-
}
9+
type (
10+
TestOptions struct {
11+
Root string
12+
RunFilter string
13+
FormatName string
14+
OutputPath string
15+
Commit bool
16+
NoRestore bool
17+
ForceRestore bool
18+
FailOnSkipped bool
19+
Color bool
20+
NoColor bool
21+
FullDiff bool
22+
NoDiff bool
23+
}
24+
25+
UpdateOptions struct {
26+
Root string
27+
RunFilter string
28+
Paths []string
29+
Commit bool
30+
NoRestore bool
31+
ForceRestore bool
32+
Pending bool
33+
Interactive bool
34+
DryRun bool
35+
}
36+
)
2337

2438
/*
2539
Init initializes a code repository for RegreSQL processing.
@@ -108,37 +122,38 @@ case and add a value for each parameter. `)
108122
Update updates the expected files from the queries and their parameters.
109123
Each query runs in its own transaction that rolls back (unless commit is true).
110124
*/
111-
func Update(root string, runFilter string, commit, noRestore, forceRestore bool) {
112-
config, err := ReadConfig(root)
125+
func Update(opts UpdateOptions) {
126+
config, err := ReadConfig(opts.Root)
113127
ignorePatterns := []string{}
114128
if err == nil {
115129
ignorePatterns = config.Ignore
116130
}
117131

118-
suite := Walk(root, ignorePatterns)
119-
suite.SetRunFilter(runFilter)
132+
suite := Walk(opts.Root, ignorePatterns)
133+
suite.SetRunFilter(opts.RunFilter)
134+
suite.SetPathFilters(opts.Paths)
120135
config, err = suite.readConfig()
121136
if err != nil {
122137
fmt.Print(err.Error())
123138
os.Exit(3)
124139
}
125140

126-
autoRestore(config, root, noRestore, forceRestore)
141+
autoRestore(config, opts.Root, opts.NoRestore, opts.ForceRestore)
127142

128143
// Validate schema hasn't changed since last snapshot build
129-
if err := ValidateSchemaHash(root); err != nil {
144+
if err := ValidateSchemaHash(opts.Root); err != nil {
130145
fmt.Printf("Error: %s\n", err)
131146
os.Exit(1)
132147
}
133148

134149
// Validate migrations haven't changed since last snapshot build
135-
if err := ValidateMigrationsHash(root); err != nil {
150+
if err := ValidateMigrationsHash(opts.Root); err != nil {
136151
fmt.Printf("Error: %s\n", err)
137152
os.Exit(1)
138153
}
139154

140155
// Validate migration command hasn't changed since last snapshot build
141-
if err := ValidateMigrationCommandHash(root); err != nil {
156+
if err := ValidateMigrationCommandHash(opts.Root); err != nil {
142157
fmt.Printf("Error: %s\n", err)
143158
os.Exit(1)
144159
}
@@ -149,16 +164,26 @@ func Update(root string, runFilter string, commit, noRestore, forceRestore bool)
149164
}
150165

151166
// Validate server settings match snapshot (warn, strict, or ignore)
152-
if err := validateServerSettings(config, root); err != nil {
167+
if err := validateServerSettings(config, opts.Root); err != nil {
153168
fmt.Printf("Error: %s\n", err)
154169
os.Exit(1)
155170
}
156171

157-
if err := suite.createExpectedResults(config.PgUri, commit); err != nil {
172+
updateOpts := createExpectedOptions{
173+
Commit: opts.Commit,
174+
Pending: opts.Pending,
175+
Interactive: opts.Interactive,
176+
DryRun: opts.DryRun,
177+
}
178+
if err := suite.createExpectedResults(config.PgUri, updateOpts); err != nil {
158179
fmt.Print(err.Error())
159180
os.Exit(12)
160181
}
161182

183+
if opts.DryRun {
184+
return // Don't print success message for dry-run
185+
}
186+
162187
fmt.Println("")
163188
fmt.Println(`Expected files have now been created.
164189
You can run regression tests for your SQL queries with the command

regresql/resultset.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,31 @@ func (r *ResultSet) Write(filename string, overwrite bool) error {
168168
return nil
169169
}
170170

171+
// LoadResultSet loads a ResultSet from a JSON file
172+
func LoadResultSet(filename string) (*ResultSet, error) {
173+
data, err := os.ReadFile(filename)
174+
if err != nil {
175+
return nil, fmt.Errorf("failed to read file '%s': %w", filename, err)
176+
}
177+
178+
var rs ResultSet
179+
if err := json.Unmarshal(data, &rs); err != nil {
180+
return nil, fmt.Errorf("failed to parse JSON from '%s': %w", filename, err)
181+
}
182+
183+
rs.Filename = filename
184+
return &rs, nil
185+
}
186+
187+
// ToJSON returns the JSON representation of the ResultSet as a string
188+
func (r *ResultSet) ToJSON() string {
189+
jsonBytes, err := json.Marshal(r)
190+
if err != nil {
191+
return ""
192+
}
193+
return string(jsonBytes)
194+
}
195+
171196
// valueToString is an helper function for the Pretty Printer
172197
func valueToString(value any) string {
173198
switch v := value.(type) {

0 commit comments

Comments
 (0)