diff --git a/go.mod b/go.mod index 4193df22e4..61daf8a1ce 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220626175859-9abda183db8e github.com/bytecodealliance/wasmtime-go v1.0.0 + github.com/cubicdaiya/gonp v1.0.4 github.com/davecgh/go-spew v1.1.1 github.com/go-sql-driver/mysql v1.6.0 github.com/google/go-cmp v0.5.9 diff --git a/go.sum b/go.sum index 66fd487d0a..5e17d90206 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= +github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 276fc10fb9..baa4b3d842 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -1,6 +1,8 @@ package cmd import ( + "bufio" + "bytes" "context" "fmt" "io" @@ -9,6 +11,7 @@ import ( "path/filepath" "runtime/trace" + "github.com/cubicdaiya/gonp" "github.com/spf13/cobra" "github.com/spf13/pflag" "gopkg.in/yaml.v3" @@ -31,6 +34,7 @@ func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int rootCmd.PersistentFlags().BoolP("experimental", "x", false, "enable experimental features (default: false)") rootCmd.AddCommand(checkCmd) + rootCmd.AddCommand(diffCmd) rootCmd.AddCommand(genCmd) rootCmd.AddCommand(initCmd) rootCmd.AddCommand(versionCmd) @@ -201,3 +205,48 @@ var checkCmd = &cobra.Command{ return nil }, } + +func getLines(f []byte) []string { + fp := bytes.NewReader(f) + scanner := bufio.NewScanner(fp) + lines := make([]string, 0) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines +} + +func filterHunks[T gonp.Elem](uniHunks []gonp.UniHunk[T]) []gonp.UniHunk[T] { + var out []gonp.UniHunk[T] + for i, uniHunk := range uniHunks { + var changed bool + for _, e := range uniHunk.GetChanges() { + switch e.GetType() { + case gonp.SesDelete: + changed = true + case gonp.SesAdd: + changed = true + } + } + if changed { + out = append(out, uniHunks[i]) + } + } + return out +} + +var diffCmd = &cobra.Command{ + Use: "diff", + Short: "Compare the generated files to the existing files", + RunE: func(cmd *cobra.Command, args []string) error { + if debug.Traced { + defer trace.StartRegion(cmd.Context(), "diff").End() + } + stderr := cmd.ErrOrStderr() + dir, name := getConfigPath(stderr, cmd.Flag("file")) + if err := Diff(cmd.Context(), ParseEnv(cmd), dir, name, stderr); err != nil { + os.Exit(1) + } + return nil + }, +} diff --git a/internal/cmd/diff.go b/internal/cmd/diff.go new file mode 100644 index 0000000000..dbab4c6ed8 --- /dev/null +++ b/internal/cmd/diff.go @@ -0,0 +1,62 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "runtime/trace" + "sort" + "strings" + + "github.com/cubicdaiya/gonp" + "github.com/kyleconroy/sqlc/internal/debug" +) + +func Diff(ctx context.Context, e Env, dir, name string, stderr io.Writer) error { + output, err := Generate(ctx, e, dir, name, stderr) + if err != nil { + return err + } + if debug.Traced { + defer trace.StartRegion(ctx, "checkfiles").End() + } + var errored bool + + keys := make([]string, 0, len(output)) + for k, _ := range output { + kk := k + keys = append(keys, kk) + } + sort.Strings(keys) + + for _, filename := range keys { + source := output[filename] + if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) { + errored = true + // stdout message + continue + } + existing, err := os.ReadFile(filename) + if err != nil { + errored = true + fmt.Fprintf(stderr, "%s: %s\n", filename, err) + continue + } + diff := gonp.New(getLines(existing), getLines([]byte(source))) + diff.Compose() + uniHunks := filterHunks(diff.UnifiedHunks()) + + if len(uniHunks) > 0 { + errored = true + fmt.Fprintf(stderr, "--- a%s\n", strings.TrimPrefix(filename, dir)) + fmt.Fprintf(stderr, "+++ b%s\n", strings.TrimPrefix(filename, dir)) + diff.FprintUniHunks(stderr, uniHunks) + } + } + if errored { + return errors.New("diff found") + } + return nil +} diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go index 3a3e759fba..c31d2beb5e 100644 --- a/internal/endtoend/endtoend_test.go +++ b/internal/endtoend/endtoend_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "encoding/json" "os" "path/filepath" "strings" @@ -92,14 +93,31 @@ func TestReplay(t *testing.T) { tc := replay t.Run(tc, func(t *testing.T) { t.Parallel() - path, _ := filepath.Abs(tc) + var stderr bytes.Buffer + var output map[string]string + var err error + + path, _ := filepath.Abs(tc) + args := parseExec(t, path) expected := expectedStderr(t, path) - output, err := cmd.Generate(ctx, cmd.Env{ExperimentalFeatures: true}, path, "", &stderr) + + switch args.Command { + case "diff": + err = cmd.Diff(ctx, cmd.Env{ExperimentalFeatures: true}, path, "", &stderr) + case "generate": + output, err = cmd.Generate(ctx, cmd.Env{ExperimentalFeatures: true}, path, "", &stderr) + if err == nil { + cmpDirectory(t, path, output) + } + default: + t.Fatalf("unknown command") + } + if len(expected) == 0 && err != nil { - t.Fatalf("sqlc generate failed: %s", stderr.String()) + t.Fatalf("sqlc %s failed: %s", args.Command, stderr.String()) } - cmpDirectory(t, path, output) + if diff := cmp.Diff(expected, stderr.String()); diff != "" { t.Errorf("stderr differed (-want +got):\n%s", diff) } @@ -179,6 +197,29 @@ func expectedStderr(t *testing.T, dir string) string { return "" } +type exec struct { + Command string `json:"command"` +} + +func parseExec(t *testing.T, dir string) exec { + t.Helper() + var e exec + path := filepath.Join(dir, "exec.json") + if _, err := os.Stat(path); !os.IsNotExist(err) { + blob, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(blob, &e); err != nil { + t.Fatal(err) + } + } + if e.Command == "" { + e.Command = "generate" + } + return e +} + func BenchmarkReplay(b *testing.B) { ctx := context.Background() var dirs []string diff --git a/internal/endtoend/testdata/diff_no_output/exec.json b/internal/endtoend/testdata/diff_no_output/exec.json new file mode 100644 index 0000000000..699e10bacd --- /dev/null +++ b/internal/endtoend/testdata/diff_no_output/exec.json @@ -0,0 +1,3 @@ +{ + "command": "diff" +} diff --git a/internal/endtoend/testdata/diff_no_output/go/db.go b/internal/endtoend/testdata/diff_no_output/go/db.go new file mode 100644 index 0000000000..f24aee5b7e --- /dev/null +++ b/internal/endtoend/testdata/diff_no_output/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.16.0 + +package authors + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/diff_no_output/go/models.go b/internal/endtoend/testdata/diff_no_output/go/models.go new file mode 100644 index 0000000000..815c7bab68 --- /dev/null +++ b/internal/endtoend/testdata/diff_no_output/go/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.16.0 + +package authors + +import ( + "database/sql" +) + +type Author struct { + ID int64 + Name string + Bio sql.NullString +} + +type Book struct { + ID int64 + Title string +} diff --git a/internal/endtoend/testdata/diff_no_output/go/query.sql.go b/internal/endtoend/testdata/diff_no_output/go/query.sql.go new file mode 100644 index 0000000000..d3ab21059a --- /dev/null +++ b/internal/endtoend/testdata/diff_no_output/go/query.sql.go @@ -0,0 +1,83 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.16.0 +// source: query.sql + +package authors + +import ( + "context" + "database/sql" +) + +const createAuthor = `-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + $1, $2 +) +RETURNING id, name, bio +` + +type CreateAuthorParams struct { + Name string + Bio sql.NullString +} + +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { + row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) + var i Author + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const getAuthor = `-- name: GetAuthor :one +SELECT id, name, bio FROM authors +WHERE id = $1 LIMIT 1 +` + +func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { + row := q.db.QueryRowContext(ctx, getAuthor, id) + var i Author + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const listAuthors = `-- name: ListAuthors :many +SELECT id, name, bio FROM authors +ORDER BY bio +` + +func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { + rows, err := q.db.QueryContext(ctx, listAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Author + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const selectOne = `-- name: SelectOne :one +SELECT 1 +` + +func (q *Queries) SelectOne(ctx context.Context) (interface{}, error) { + row := q.db.QueryRowContext(ctx, selectOne) + var column_1 interface{} + err := row.Scan(&column_1) + return column_1, err +} diff --git a/internal/endtoend/testdata/diff_no_output/query.sql b/internal/endtoend/testdata/diff_no_output/query.sql new file mode 100644 index 0000000000..cc98eae93c --- /dev/null +++ b/internal/endtoend/testdata/diff_no_output/query.sql @@ -0,0 +1,18 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE id = $1 LIMIT 1; + +-- name: ListAuthors :many +SELECT * FROM authors +ORDER BY bio; + +-- name: SelectOne :one +SELECT 1; + +-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + $1, $2 +) +RETURNING *; diff --git a/internal/endtoend/testdata/diff_no_output/schema.sql b/internal/endtoend/testdata/diff_no_output/schema.sql new file mode 100644 index 0000000000..baed2e0504 --- /dev/null +++ b/internal/endtoend/testdata/diff_no_output/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + title text NOT NULL +); + diff --git a/internal/endtoend/testdata/diff_no_output/sqlc.json b/internal/endtoend/testdata/diff_no_output/sqlc.json new file mode 100644 index 0000000000..3576609e21 --- /dev/null +++ b/internal/endtoend/testdata/diff_no_output/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "postgresql", + "gen": { + "go": { + "package": "authors", + "out": "go" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/diff_output/exec.json b/internal/endtoend/testdata/diff_output/exec.json new file mode 100644 index 0000000000..699e10bacd --- /dev/null +++ b/internal/endtoend/testdata/diff_output/exec.json @@ -0,0 +1,3 @@ +{ + "command": "diff" +} diff --git a/internal/endtoend/testdata/diff_output/go/db.go b/internal/endtoend/testdata/diff_output/go/db.go new file mode 100644 index 0000000000..f24aee5b7e --- /dev/null +++ b/internal/endtoend/testdata/diff_output/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.16.0 + +package authors + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/diff_output/go/models.go b/internal/endtoend/testdata/diff_output/go/models.go new file mode 100644 index 0000000000..53572f834f --- /dev/null +++ b/internal/endtoend/testdata/diff_output/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.16.0 + +package authors + +import ( + "database/sql" +) + +type Author struct { + ID int64 + Name string + Bio sql.NullString +} diff --git a/internal/endtoend/testdata/diff_output/go/query.sql.go b/internal/endtoend/testdata/diff_output/go/query.sql.go new file mode 100644 index 0000000000..27bc1b250a --- /dev/null +++ b/internal/endtoend/testdata/diff_output/go/query.sql.go @@ -0,0 +1,82 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.16.0 +// source: query.sql + +package authors + +import ( + "context" + "database/sql" +) + +const createAuthor = `-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + $1, $2 +) +RETURNING id, name, bio +` + +type CreateAuthorParams struct { + Name string + Bio sql.NullString +} + +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { + row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) + var i Author + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const deleteAuthor = `-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = $1 +` + +func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteAuthor, id) + return err +} + +const getAuthor = `-- name: GetAuthor :one +SELECT id, name, bio FROM authors +WHERE id = $1 LIMIT 1 +` + +func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { + row := q.db.QueryRowContext(ctx, getAuthor, id) + var i Author + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const listAuthors = `-- name: ListAuthors :many +SELECT id, name, bio FROM authors +ORDER BY name +` + +func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { + rows, err := q.db.QueryContext(ctx, listAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Author + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/diff_output/query.sql b/internal/endtoend/testdata/diff_output/query.sql new file mode 100644 index 0000000000..cc98eae93c --- /dev/null +++ b/internal/endtoend/testdata/diff_output/query.sql @@ -0,0 +1,18 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE id = $1 LIMIT 1; + +-- name: ListAuthors :many +SELECT * FROM authors +ORDER BY bio; + +-- name: SelectOne :one +SELECT 1; + +-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + $1, $2 +) +RETURNING *; diff --git a/internal/endtoend/testdata/diff_output/schema.sql b/internal/endtoend/testdata/diff_output/schema.sql new file mode 100644 index 0000000000..baed2e0504 --- /dev/null +++ b/internal/endtoend/testdata/diff_output/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + title text NOT NULL +); + diff --git a/internal/endtoend/testdata/diff_output/sqlc.json b/internal/endtoend/testdata/diff_output/sqlc.json new file mode 100644 index 0000000000..3576609e21 --- /dev/null +++ b/internal/endtoend/testdata/diff_output/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "postgresql", + "gen": { + "go": { + "package": "authors", + "out": "go" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/diff_output/stderr.txt b/internal/endtoend/testdata/diff_output/stderr.txt new file mode 100644 index 0000000000..52dcd38708 --- /dev/null +++ b/internal/endtoend/testdata/diff_output/stderr.txt @@ -0,0 +1,54 @@ +--- a/go/models.go ++++ b/go/models.go +@@ -13,3 +13,8 @@ + Name string + Bio sql.NullString + } ++ ++type Book struct { ++ ID int64 ++ Title string ++} +--- a/go/query.sql.go ++++ b/go/query.sql.go +@@ -31,16 +31,6 @@ + return i, err + } + +-const deleteAuthor = `-- name: DeleteAuthor :exec +-DELETE FROM authors +-WHERE id = $1 +-` +- +-func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { +- _, err := q.db.ExecContext(ctx, deleteAuthor, id) +- return err +-} +- + const getAuthor = `-- name: GetAuthor :one + SELECT id, name, bio FROM authors + WHERE id = $1 LIMIT 1 +@@ -55,7 +45,7 @@ + + const listAuthors = `-- name: ListAuthors :many + SELECT id, name, bio FROM authors ++ORDER BY bio +-ORDER BY name + ` + + func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { +@@ -80,3 +70,14 @@ + } + return items, nil + } ++ ++const selectOne = `-- name: SelectOne :one ++SELECT 1 ++` ++ ++func (q *Queries) SelectOne(ctx context.Context) (interface{}, error) { ++ row := q.db.QueryRowContext(ctx, selectOne) ++ var column_1 interface{} ++ err := row.Scan(&column_1) ++ return column_1, err ++}