Skip to content

Commit f9a9e94

Browse files
authored
Add visualization management via CLI (#48)
1 parent beeb106 commit f9a9e94

9 files changed

Lines changed: 297 additions & 5 deletions

File tree

cli/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ var rootCmd = &cobra.Command{
4040
" - Search datasets by keyword, contract address, category, or blockchain\n" +
4141
" - Create, update, archive, and retrieve saved DuneSQL queries\n" +
4242
" - Execute saved queries or raw DuneSQL and display results\n" +
43+
" - Create and manage visualizations (charts, tables, counters) on query results\n" +
4344
" - Browse Dune documentation for DuneSQL syntax, API references, and guides\n" +
45+
" - Query real-time wallet and token data via the Sim API\n" +
4446
" - Monitor credit usage, storage consumption, and billing periods\n\n" +
4547
"Authenticate with an API key via --api-key, the DUNE_API_KEY environment variable,\n" +
4648
"or by running `dune auth`.",

cmd/query/run_sql.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ func newRunSQLCmd() *cobra.Command {
1212
Use: "run-sql",
1313
Short: "Execute a raw DuneSQL query inline and display results",
1414
Long: "Execute an inline SQL statement in DuneSQL dialect without saving it as a\n" +
15-
"query on Dune. Ideal for ad-hoc exploration and one-off analysis.\n\n" +
15+
"query on Dune. This is the preferred command for running SQL when you don't\n" +
16+
"need to save the query or create visualizations.\n\n" +
17+
"Use this for ad-hoc exploration, one-off analysis, and answering data questions.\n" +
18+
"Only use 'dune query create' + 'dune query run' when you specifically need a\n" +
19+
"saved query ID (e.g. for attaching visualizations).\n\n" +
1620
"By default, waits for completion (polling every 2 seconds) and displays result rows.\n" +
1721
"Use --no-wait to submit the execution and exit immediately with just the\n" +
1822
"execution ID. Credits are consumed based on actual compute resources used.\n\n" +

cmd/visualization/delete.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package visualization
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/duneanalytics/cli/cmdutil"
8+
"github.com/duneanalytics/cli/output"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func newDeleteCmd() *cobra.Command {
13+
cmd := &cobra.Command{
14+
Use: "delete <visualization_id>",
15+
Short: "Permanently delete a visualization by its ID",
16+
Long: "Permanently delete a visualization by its ID. This action cannot be undone.\n\n" +
17+
"Use 'dune viz list --query-id <id>' to find visualization IDs for a given query.\n\n" +
18+
"Examples:\n" +
19+
" dune viz delete 12345\n" +
20+
" dune viz delete 12345 -o json",
21+
Args: cobra.ExactArgs(1),
22+
RunE: runDelete,
23+
}
24+
25+
output.AddFormatFlag(cmd, "text")
26+
27+
return cmd
28+
}
29+
30+
func runDelete(cmd *cobra.Command, args []string) error {
31+
vizID, err := strconv.Atoi(args[0])
32+
if err != nil {
33+
return fmt.Errorf("invalid visualization ID %q: must be an integer", args[0])
34+
}
35+
36+
client := cmdutil.ClientFromCmd(cmd)
37+
38+
_, err = client.DeleteVisualization(vizID)
39+
if err != nil {
40+
return err
41+
}
42+
43+
w := cmd.OutOrStdout()
44+
switch output.FormatFromCmd(cmd) {
45+
case output.FormatJSON:
46+
return output.PrintJSON(w, map[string]any{"ok": true})
47+
default:
48+
fmt.Fprintf(w, "Deleted visualization %d\n", vizID)
49+
return nil
50+
}
51+
}

cmd/visualization/get.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package visualization
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/duneanalytics/cli/cmdutil"
8+
"github.com/duneanalytics/cli/output"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func newGetCmd() *cobra.Command {
13+
cmd := &cobra.Command{
14+
Use: "get <visualization_id>",
15+
Short: "Get details of a visualization including type, options, and timestamps",
16+
Long: "Retrieve detailed information about a visualization by its ID.\n\n" +
17+
"Returns the visualization name, type, description, options JSON, query ID,\n" +
18+
"and timestamps. Use this to inspect an existing visualization before\n" +
19+
"updating it, or to review the options format for a given type.\n\n" +
20+
"Use -o json to get the full options object for programmatic use.\n\n" +
21+
"Examples:\n" +
22+
" dune viz get 12345\n" +
23+
" dune viz get 12345 -o json",
24+
Args: cobra.ExactArgs(1),
25+
RunE: runGet,
26+
}
27+
28+
output.AddFormatFlag(cmd, "text")
29+
30+
return cmd
31+
}
32+
33+
func runGet(cmd *cobra.Command, args []string) error {
34+
vizID, err := strconv.Atoi(args[0])
35+
if err != nil {
36+
return fmt.Errorf("invalid visualization ID %q: must be an integer", args[0])
37+
}
38+
39+
client := cmdutil.ClientFromCmd(cmd)
40+
41+
resp, err := client.GetVisualization(vizID)
42+
if err != nil {
43+
return err
44+
}
45+
46+
w := cmd.OutOrStdout()
47+
switch output.FormatFromCmd(cmd) {
48+
case output.FormatJSON:
49+
return output.PrintJSON(w, resp)
50+
default:
51+
output.PrintTable(w,
52+
[]string{"Field", "Value"},
53+
[][]string{
54+
{"ID", fmt.Sprintf("%d", resp.ID)},
55+
{"Query ID", fmt.Sprintf("%d", resp.QueryID)},
56+
{"Name", resp.Name},
57+
{"Description", resp.Description},
58+
{"Type", resp.Type},
59+
{"Created At", resp.CreatedAt},
60+
{"Updated At", resp.UpdatedAt},
61+
},
62+
)
63+
return nil
64+
}
65+
}

cmd/visualization/list.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package visualization
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/duneanalytics/cli/cmdutil"
7+
"github.com/duneanalytics/cli/output"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
func newListCmd() *cobra.Command {
12+
cmd := &cobra.Command{
13+
Use: "list",
14+
Short: "List all visualizations attached to a query",
15+
Long: "List all visualizations attached to a given query.\n\n" +
16+
"Use this to discover what charts, tables, and counters already exist\n" +
17+
"for a query before creating new ones. Each result includes the\n" +
18+
"visualization ID, name, type, and timestamps.\n\n" +
19+
"Use 'dune viz get <id>' with a specific ID to fetch full details\n" +
20+
"including chart options.\n\n" +
21+
"Examples:\n" +
22+
" dune viz list --query-id 12345\n" +
23+
" dune viz list --query-id 12345 --limit 10 --offset 0 -o json",
24+
RunE: runList,
25+
}
26+
27+
cmd.Flags().Int("query-id", 0, "ID of the query to list visualizations for (required)")
28+
cmd.Flags().Int("limit", 25, "maximum number of results to return")
29+
cmd.Flags().Int("offset", 0, "number of results to skip")
30+
_ = cmd.MarkFlagRequired("query-id")
31+
output.AddFormatFlag(cmd, "text")
32+
33+
return cmd
34+
}
35+
36+
func runList(cmd *cobra.Command, _ []string) error {
37+
queryID, _ := cmd.Flags().GetInt("query-id")
38+
limit, _ := cmd.Flags().GetInt("limit")
39+
offset, _ := cmd.Flags().GetInt("offset")
40+
41+
client := cmdutil.ClientFromCmd(cmd)
42+
43+
resp, err := client.ListQueryVisualizations(queryID, limit, offset)
44+
if err != nil {
45+
return err
46+
}
47+
48+
w := cmd.OutOrStdout()
49+
switch output.FormatFromCmd(cmd) {
50+
case output.FormatJSON:
51+
return output.PrintJSON(w, resp)
52+
default:
53+
rows := make([][]string, len(resp.Results))
54+
for i, v := range resp.Results {
55+
rows[i] = []string{
56+
fmt.Sprintf("%d", v.ID),
57+
v.Name,
58+
v.Type,
59+
v.CreatedAt,
60+
}
61+
}
62+
output.PrintTable(w, []string{"ID", "Name", "Type", "Created"}, rows)
63+
return nil
64+
}
65+
}

cmd/visualization/update.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package visualization
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/duneanalytics/cli/cmdutil"
9+
"github.com/duneanalytics/cli/output"
10+
"github.com/duneanalytics/duneapi-client-go/models"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
func newUpdateCmd() *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "update <visualization_id>",
17+
Short: "Update an existing visualization",
18+
Long: "Replace a visualization's properties. Fetches the current visualization first,\n" +
19+
"applies your changes, and sends the full updated object.\n\n" +
20+
"Only supply the flags you want to change; unchanged fields are preserved.\n" +
21+
"At least one flag must be provided.\n\n" +
22+
"Examples:\n" +
23+
" dune viz update 12345 --name \"New Name\"\n" +
24+
" dune viz update 12345 --type counter --options '{\"counterColName\":\"count\"}'",
25+
Args: cobra.ExactArgs(1),
26+
RunE: runUpdate,
27+
}
28+
29+
cmd.Flags().String("name", "", "new name for the visualization")
30+
cmd.Flags().String("type", "", "new visualization type")
31+
cmd.Flags().String("description", "", "new description for the visualization")
32+
cmd.Flags().String("options", "", "new visualization options JSON")
33+
output.AddFormatFlag(cmd, "text")
34+
35+
return cmd
36+
}
37+
38+
func runUpdate(cmd *cobra.Command, args []string) error {
39+
vizID, err := strconv.Atoi(args[0])
40+
if err != nil {
41+
return fmt.Errorf("invalid visualization ID %q: must be an integer", args[0])
42+
}
43+
44+
if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("type") &&
45+
!cmd.Flags().Changed("description") && !cmd.Flags().Changed("options") {
46+
return fmt.Errorf("at least one flag must be provided (--name, --type, --description, or --options)")
47+
}
48+
49+
client := cmdutil.ClientFromCmd(cmd)
50+
51+
// Fetch current visualization to preserve unchanged fields
52+
current, err := client.GetVisualization(vizID)
53+
if err != nil {
54+
return fmt.Errorf("failed to fetch current visualization: %w", err)
55+
}
56+
57+
// Start with current values, override with changed flags
58+
req := models.UpdateVisualizationRequest{
59+
Name: current.Name,
60+
Type: current.Type,
61+
Description: current.Description,
62+
Options: current.Options,
63+
}
64+
65+
if cmd.Flags().Changed("name") {
66+
req.Name, _ = cmd.Flags().GetString("name")
67+
}
68+
if cmd.Flags().Changed("type") {
69+
req.Type, _ = cmd.Flags().GetString("type")
70+
}
71+
if cmd.Flags().Changed("description") {
72+
req.Description, _ = cmd.Flags().GetString("description")
73+
}
74+
if cmd.Flags().Changed("options") {
75+
optionsStr, _ := cmd.Flags().GetString("options")
76+
var options map[string]any
77+
if err := json.Unmarshal([]byte(optionsStr), &options); err != nil {
78+
return fmt.Errorf("invalid --options JSON: %w", err)
79+
}
80+
req.Options = options
81+
}
82+
83+
resp, err := client.UpdateVisualization(vizID, req)
84+
if err != nil {
85+
return err
86+
}
87+
88+
w := cmd.OutOrStdout()
89+
switch output.FormatFromCmd(cmd) {
90+
case output.FormatJSON:
91+
return output.PrintJSON(w, resp)
92+
default:
93+
fmt.Fprintf(w, "Updated visualization %d\n", resp.ID)
94+
return nil
95+
}
96+
}

cmd/visualization/visualization.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,19 @@ func NewVisualizationCmd() *cobra.Command {
99
Short: "Create and manage Dune visualizations",
1010
Long: "Create and manage visualizations on Dune queries.\n\n" +
1111
"Visualizations are charts, tables, counters, and other visual representations\n" +
12-
"of query results. Each visualization is attached to a saved query.",
12+
"of query results. Each visualization is attached to a saved query by its query ID.\n\n" +
13+
"Important: Visualizations require a saved query ID (from 'dune query create').\n" +
14+
"'dune query run-sql' does not create a saved query and cannot be used with\n" +
15+
"visualizations.\n\n" +
16+
"Supported types: chart, table, counter, pivot, cohort, funnel, choropleth,\n" +
17+
"sankey, sunburst_sequence, word_cloud.",
1318
}
1419

1520
cmd.AddCommand(newCreateCmd())
21+
cmd.AddCommand(newGetCmd())
22+
cmd.AddCommand(newUpdateCmd())
23+
cmd.AddCommand(newDeleteCmd())
24+
cmd.AddCommand(newListCmd())
1625

1726
return cmd
1827
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.25.6
55
require (
66
github.com/amplitude/analytics-go v1.3.0
77
github.com/charmbracelet/fang v0.4.4
8-
github.com/duneanalytics/duneapi-client-go v0.4.6
8+
github.com/duneanalytics/duneapi-client-go v0.4.7
99
github.com/modelcontextprotocol/go-sdk v1.4.0
1010
github.com/spf13/cobra v1.10.2
1111
github.com/stretchr/testify v1.11.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV
3131
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
3232
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3333
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34-
github.com/duneanalytics/duneapi-client-go v0.4.6 h1:ZjW/H86Da6RY3K9rQFUV3pU1w8kTVlgSGH3k4v3VP7Q=
35-
github.com/duneanalytics/duneapi-client-go v0.4.6/go.mod h1:7pXXufWvR/Mh2KOehdyBaunJXmHI+pzjUmyQTQhJjdE=
34+
github.com/duneanalytics/duneapi-client-go v0.4.7 h1:bsyMlKbTZnU3m7aXfNLjJNXuk0xKtucK5vEwEAHtELk=
35+
github.com/duneanalytics/duneapi-client-go v0.4.7/go.mod h1:7pXXufWvR/Mh2KOehdyBaunJXmHI+pzjUmyQTQhJjdE=
3636
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
3737
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
3838
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=

0 commit comments

Comments
 (0)