From c6b1e8f97e9b9303eaae021204d2422071571c0f Mon Sep 17 00:00:00 2001 From: Ivan Pusic <450140+ivpusic@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:30:24 +0100 Subject: [PATCH] Query update CLI option --- cmd/query/query.go | 1 + cmd/query/testutil_test.go | 5 ++ cmd/query/update.go | 85 +++++++++++++++++++++++++ cmd/query/update_test.go | 124 +++++++++++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 cmd/query/update.go create mode 100644 cmd/query/update_test.go diff --git a/cmd/query/query.go b/cmd/query/query.go index d5dabe1..0c4d6c2 100644 --- a/cmd/query/query.go +++ b/cmd/query/query.go @@ -10,5 +10,6 @@ func NewQueryCmd() *cobra.Command { } cmd.AddCommand(newCreateCmd()) cmd.AddCommand(newGetCmd()) + cmd.AddCommand(newUpdateCmd()) return cmd } diff --git a/cmd/query/testutil_test.go b/cmd/query/testutil_test.go index 736da91..49b0d78 100644 --- a/cmd/query/testutil_test.go +++ b/cmd/query/testutil_test.go @@ -16,6 +16,7 @@ type mockClient struct { dune.DuneClient createQueryFn func(models.CreateQueryRequest) (*models.CreateQueryResponse, error) getQueryFn func(int) (*models.GetQueryResponse, error) + updateQueryFn func(int, models.UpdateQueryRequest) (*models.UpdateQueryResponse, error) } func (m *mockClient) CreateQuery(req models.CreateQueryRequest) (*models.CreateQueryResponse, error) { @@ -26,6 +27,10 @@ func (m *mockClient) GetQuery(queryID int) (*models.GetQueryResponse, error) { return m.getQueryFn(queryID) } +func (m *mockClient) UpdateQuery(queryID int, req models.UpdateQueryRequest) (*models.UpdateQueryResponse, error) { + return m.updateQueryFn(queryID, req) +} + // newTestRoot builds a root → query command tree with the mock injected. func newTestRoot(mock dune.DuneClient) (*cobra.Command, *bytes.Buffer) { root := &cobra.Command{ diff --git a/cmd/query/update.go b/cmd/query/update.go new file mode 100644 index 0000000..5ec711d --- /dev/null +++ b/cmd/query/update.go @@ -0,0 +1,85 @@ +package query + +import ( + "fmt" + "strconv" + + "github.com/duneanalytics/cli/cmdutil" + "github.com/duneanalytics/cli/output" + "github.com/duneanalytics/duneapi-client-go/models" + "github.com/spf13/cobra" +) + +func newUpdateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update an existing saved query", + Args: cobra.ExactArgs(1), + RunE: runUpdate, + } + + cmd.Flags().String("name", "", "query name") + cmd.Flags().String("sql", "", "query SQL") + cmd.Flags().String("description", "", "query description") + cmd.Flags().Bool("private", false, "make the query private") + cmd.Flags().StringSlice("tags", nil, "query tags (comma-separated)") + output.AddFormatFlag(cmd, "text") + + return cmd +} + +func runUpdate(cmd *cobra.Command, args []string) error { + queryID, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid query ID %q: must be an integer", args[0]) + } + + var req models.UpdateQueryRequest + changed := false + + if cmd.Flags().Changed("name") { + v, _ := cmd.Flags().GetString("name") + req.Name = &v + changed = true + } + if cmd.Flags().Changed("sql") { + v, _ := cmd.Flags().GetString("sql") + req.QuerySQL = &v + changed = true + } + if cmd.Flags().Changed("description") { + v, _ := cmd.Flags().GetString("description") + req.Description = &v + changed = true + } + if cmd.Flags().Changed("private") { + v, _ := cmd.Flags().GetBool("private") + req.IsPrivate = &v + changed = true + } + if cmd.Flags().Changed("tags") { + v, _ := cmd.Flags().GetStringSlice("tags") + req.Tags = v + changed = true + } + + if !changed { + return fmt.Errorf("at least one flag must be provided (--name, --sql, --description, --private, or --tags)") + } + + client := cmdutil.ClientFromCmd(cmd) + + resp, err := client.UpdateQuery(queryID, req) + if err != nil { + return err + } + + w := cmd.OutOrStdout() + switch output.FormatFromCmd(cmd) { + case output.FormatJSON: + return output.PrintJSON(w, resp) + default: + fmt.Fprintf(w, "Updated query %d\n", resp.QueryID) + return nil + } +} diff --git a/cmd/query/update_test.go b/cmd/query/update_test.go new file mode 100644 index 0000000..3df9f7d --- /dev/null +++ b/cmd/query/update_test.go @@ -0,0 +1,124 @@ +package query_test + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/duneanalytics/duneapi-client-go/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateSingleFlag(t *testing.T) { + mock := &mockClient{ + updateQueryFn: func(id int, req models.UpdateQueryRequest) (*models.UpdateQueryResponse, error) { + assert.Equal(t, 4125432, id) + require.NotNil(t, req.Name) + assert.Equal(t, "New", *req.Name) + assert.Nil(t, req.QuerySQL) + assert.Nil(t, req.Description) + assert.Nil(t, req.IsPrivate) + assert.Nil(t, req.Tags) + return &models.UpdateQueryResponse{QueryID: 4125432}, nil + }, + } + + root, buf := newTestRoot(mock) + root.SetArgs([]string{"query", "update", "4125432", "--name", "New"}) + require.NoError(t, root.Execute()) + assert.Equal(t, "Updated query 4125432\n", buf.String()) +} + +func TestUpdateMultipleFlags(t *testing.T) { + mock := &mockClient{ + updateQueryFn: func(_ int, req models.UpdateQueryRequest) (*models.UpdateQueryResponse, error) { + require.NotNil(t, req.Name) + assert.Equal(t, "New", *req.Name) + require.NotNil(t, req.QuerySQL) + assert.Equal(t, "SELECT 2", *req.QuerySQL) + assert.Equal(t, []string{"defi", "uniswap"}, req.Tags) + assert.Nil(t, req.Description) + assert.Nil(t, req.IsPrivate) + return &models.UpdateQueryResponse{QueryID: 1}, nil + }, + } + + root, _ := newTestRoot(mock) + root.SetArgs([]string{"query", "update", "1", "--name", "New", "--sql", "SELECT 2", "--tags", "defi,uniswap"}) + require.NoError(t, root.Execute()) +} + +func TestUpdatePrivateFlag(t *testing.T) { + mock := &mockClient{ + updateQueryFn: func(_ int, req models.UpdateQueryRequest) (*models.UpdateQueryResponse, error) { + require.NotNil(t, req.IsPrivate) + assert.True(t, *req.IsPrivate) + return &models.UpdateQueryResponse{QueryID: 1}, nil + }, + } + + root, _ := newTestRoot(mock) + root.SetArgs([]string{"query", "update", "1", "--private"}) + require.NoError(t, root.Execute()) +} + +func TestUpdatePrivateFalse(t *testing.T) { + mock := &mockClient{ + updateQueryFn: func(_ int, req models.UpdateQueryRequest) (*models.UpdateQueryResponse, error) { + require.NotNil(t, req.IsPrivate) + assert.False(t, *req.IsPrivate) + return &models.UpdateQueryResponse{QueryID: 1}, nil + }, + } + + root, _ := newTestRoot(mock) + root.SetArgs([]string{"query", "update", "1", "--private=false"}) + require.NoError(t, root.Execute()) +} + +func TestUpdateNoFlags(t *testing.T) { + root, _ := newTestRoot(&mockClient{}) + root.SetArgs([]string{"query", "update", "1"}) + err := root.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "at least one flag") +} + +func TestUpdateNonIntegerID(t *testing.T) { + root, _ := newTestRoot(&mockClient{}) + root.SetArgs([]string{"query", "update", "abc", "--name", "X"}) + err := root.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid query ID") +} + +func TestUpdateAPIError(t *testing.T) { + mock := &mockClient{ + updateQueryFn: func(_ int, _ models.UpdateQueryRequest) (*models.UpdateQueryResponse, error) { + return nil, errors.New("api: unauthorized") + }, + } + + root, _ := newTestRoot(mock) + root.SetArgs([]string{"query", "update", "1", "--name", "X"}) + err := root.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "api: unauthorized") +} + +func TestUpdateJSONOutput(t *testing.T) { + mock := &mockClient{ + updateQueryFn: func(_ int, _ models.UpdateQueryRequest) (*models.UpdateQueryResponse, error) { + return &models.UpdateQueryResponse{QueryID: 4125432}, nil + }, + } + + root, buf := newTestRoot(mock) + root.SetArgs([]string{"query", "update", "4125432", "--name", "New", "-o", "json"}) + require.NoError(t, root.Execute()) + + var got map[string]int + require.NoError(t, json.Unmarshal(buf.Bytes(), &got)) + assert.Equal(t, 4125432, got["query_id"]) +}