-
Notifications
You must be signed in to change notification settings - Fork 5
Execution results APP-7350 #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package execution | ||
|
|
||
| import "github.com/spf13/cobra" | ||
|
|
||
| // NewExecutionCmd returns the `execution` parent command. | ||
| func NewExecutionCmd() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "execution", | ||
| Short: "Manage query executions", | ||
| } | ||
| cmd.AddCommand(newResultsCmd()) | ||
| return cmd | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package execution | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/duneanalytics/cli/cmdutil" | ||
| "github.com/duneanalytics/cli/output" | ||
| "github.com/duneanalytics/duneapi-client-go/models" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func newResultsCmd() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "results <execution-id>", | ||
| Short: "Fetch results of a query execution", | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: runResults, | ||
| } | ||
|
|
||
| cmd.Flags().Int("limit", 0, "maximum number of rows to return (0 = all)") | ||
| cmd.Flags().Int("offset", 0, "number of rows to skip") | ||
| output.AddFormatFlag(cmd, "text") | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| func runResults(cmd *cobra.Command, args []string) error { | ||
| executionID := args[0] | ||
|
|
||
| limit, _ := cmd.Flags().GetInt("limit") | ||
| offset, _ := cmd.Flags().GetInt("offset") | ||
|
|
||
| if limit < 0 { | ||
| return fmt.Errorf("limit must be non-negative, got %d", limit) | ||
| } | ||
| if offset < 0 { | ||
| return fmt.Errorf("offset must be non-negative, got %d", offset) | ||
| } | ||
|
|
||
| opts := models.ResultOptions{} | ||
| if limit > 0 || offset > 0 { | ||
| opts.Page = &models.ResultPageOption{ | ||
| Offset: uint64(offset), | ||
| Limit: uint32(limit), | ||
| } | ||
| } | ||
|
|
||
| client := cmdutil.ClientFromCmd(cmd) | ||
| resp, err := client.QueryResultsV2(executionID, opts) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| switch resp.State { | ||
| case "QUERY_STATE_COMPLETED": | ||
| return output.DisplayResults(cmd, resp) | ||
| case "QUERY_STATE_PENDING", "QUERY_STATE_EXECUTING": | ||
| w := cmd.OutOrStdout() | ||
| switch output.FormatFromCmd(cmd) { | ||
| case output.FormatJSON: | ||
| return output.PrintJSON(w, resp) | ||
| default: | ||
| fmt.Fprintf(w, "Execution ID: %s\n", executionID) | ||
| fmt.Fprintf(w, "State: %s\n", resp.State) | ||
| return nil | ||
| } | ||
| case "QUERY_STATE_FAILED": | ||
| msg := "execution failed" | ||
| if resp.Error != nil { | ||
| msg = resp.Error.Message | ||
| } | ||
| return fmt.Errorf("%s", msg) | ||
| case "QUERY_STATE_CANCELLED": | ||
| return fmt.Errorf("execution was cancelled") | ||
| default: | ||
| return fmt.Errorf("unexpected execution state: %s", resp.State) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Known
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| package execution_test | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "errors" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/duneanalytics/duneapi-client-go/models" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| var testResultsResponse = &models.ResultsResponse{ | ||
| QueryID: 4125432, | ||
| State: "QUERY_STATE_COMPLETED", | ||
| ExecutionEndedAt: ptrTime(time.Now()), | ||
| IsExecutionFinished: true, | ||
| Result: models.Result{ | ||
| Metadata: models.ResultMetadata{ | ||
| ColumnNames: []string{"block_number", "tx_hash"}, | ||
| RowCount: 2, | ||
| }, | ||
| Rows: []map[string]any{ | ||
| {"block_number": float64(100), "tx_hash": "0xabc"}, | ||
| {"block_number": float64(200), "tx_hash": "0xdef"}, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| func ptrTime(t time.Time) *time.Time { return &t } | ||
|
|
||
| func TestResultsSuccess(t *testing.T) { | ||
| mock := &mockClient{ | ||
| queryResultsV2Fn: func(id string, _ models.ResultOptions) (*models.ResultsResponse, error) { | ||
| assert.Equal(t, "01ABCDEFGHIJKLMNOPQRSTUV", id) | ||
| return testResultsResponse, nil | ||
| }, | ||
| } | ||
|
|
||
| root, buf := newTestRoot(mock) | ||
| root.SetArgs([]string{"execution", "results", "01ABCDEFGHIJKLMNOPQRSTUV"}) | ||
| require.NoError(t, root.Execute()) | ||
|
|
||
| out := buf.String() | ||
| assert.Contains(t, out, "block_number") | ||
| assert.Contains(t, out, "tx_hash") | ||
| assert.Contains(t, out, "100") | ||
| assert.Contains(t, out, "0xabc") | ||
| assert.Contains(t, out, "2 rows") | ||
| } | ||
|
|
||
| func TestResultsJSONOutput(t *testing.T) { | ||
| mock := &mockClient{ | ||
| queryResultsV2Fn: func(_ string, _ models.ResultOptions) (*models.ResultsResponse, error) { | ||
| return testResultsResponse, nil | ||
| }, | ||
| } | ||
|
|
||
| root, buf := newTestRoot(mock) | ||
| root.SetArgs([]string{"execution", "results", "01ABC", "-o", "json"}) | ||
| require.NoError(t, root.Execute()) | ||
|
|
||
| var got models.ResultsResponse | ||
| require.NoError(t, json.Unmarshal(buf.Bytes(), &got)) | ||
| assert.Equal(t, int64(4125432), got.QueryID) | ||
| assert.Equal(t, "QUERY_STATE_COMPLETED", got.State) | ||
| } | ||
|
|
||
| func TestResultsPending(t *testing.T) { | ||
| mock := &mockClient{ | ||
| queryResultsV2Fn: func(_ string, _ models.ResultOptions) (*models.ResultsResponse, error) { | ||
| return &models.ResultsResponse{ | ||
| State: "QUERY_STATE_PENDING", | ||
| }, nil | ||
| }, | ||
| } | ||
|
|
||
| root, buf := newTestRoot(mock) | ||
| root.SetArgs([]string{"execution", "results", "01ABC"}) | ||
| require.NoError(t, root.Execute()) | ||
|
|
||
| out := buf.String() | ||
| assert.Contains(t, out, "Execution ID: 01ABC") | ||
| assert.Contains(t, out, "State: QUERY_STATE_PENDING") | ||
| } | ||
|
|
||
| func TestResultsExecuting(t *testing.T) { | ||
| mock := &mockClient{ | ||
| queryResultsV2Fn: func(_ string, _ models.ResultOptions) (*models.ResultsResponse, error) { | ||
| return &models.ResultsResponse{ | ||
| State: "QUERY_STATE_EXECUTING", | ||
| }, nil | ||
| }, | ||
| } | ||
|
|
||
| root, buf := newTestRoot(mock) | ||
| root.SetArgs([]string{"execution", "results", "01ABC"}) | ||
| require.NoError(t, root.Execute()) | ||
|
|
||
| out := buf.String() | ||
| assert.Contains(t, out, "State: QUERY_STATE_EXECUTING") | ||
| } | ||
|
|
||
| func TestResultsFailed(t *testing.T) { | ||
| mock := &mockClient{ | ||
| queryResultsV2Fn: func(_ string, _ models.ResultOptions) (*models.ResultsResponse, error) { | ||
| return &models.ResultsResponse{ | ||
| State: "QUERY_STATE_FAILED", | ||
| Error: &models.ExecutionError{ | ||
| Type: "EXECUTION_ERROR", | ||
| Message: "syntax error at line 1", | ||
| }, | ||
| }, nil | ||
| }, | ||
| } | ||
|
|
||
| root, _ := newTestRoot(mock) | ||
| root.SetArgs([]string{"execution", "results", "01ABC"}) | ||
| err := root.Execute() | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "syntax error at line 1") | ||
| } | ||
|
|
||
| func TestResultsCancelled(t *testing.T) { | ||
| now := time.Now() | ||
| mock := &mockClient{ | ||
| queryResultsV2Fn: func(_ string, _ models.ResultOptions) (*models.ResultsResponse, error) { | ||
| return &models.ResultsResponse{ | ||
| State: "QUERY_STATE_CANCELLED", | ||
| CancelledAt: &now, | ||
| }, nil | ||
| }, | ||
| } | ||
|
|
||
| root, _ := newTestRoot(mock) | ||
| root.SetArgs([]string{"execution", "results", "01ABC"}) | ||
| err := root.Execute() | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "cancelled") | ||
| } | ||
|
|
||
| func TestResultsWithLimitAndOffset(t *testing.T) { | ||
| var capturedOpts models.ResultOptions | ||
| mock := &mockClient{ | ||
| queryResultsV2Fn: func(_ string, opts models.ResultOptions) (*models.ResultsResponse, error) { | ||
| capturedOpts = opts | ||
| return testResultsResponse, nil | ||
| }, | ||
| } | ||
|
|
||
| root, _ := newTestRoot(mock) | ||
| root.SetArgs([]string{"execution", "results", "01ABC", "--limit", "10", "--offset", "5"}) | ||
| require.NoError(t, root.Execute()) | ||
|
|
||
| require.NotNil(t, capturedOpts.Page) | ||
| assert.Equal(t, uint32(10), capturedOpts.Page.Limit) | ||
| assert.Equal(t, uint64(5), capturedOpts.Page.Offset) | ||
| } | ||
|
|
||
| func TestResultsAPIError(t *testing.T) { | ||
| mock := &mockClient{ | ||
| queryResultsV2Fn: func(_ string, _ models.ResultOptions) (*models.ResultsResponse, error) { | ||
| return nil, errors.New("api: connection refused") | ||
| }, | ||
| } | ||
|
|
||
| root, _ := newTestRoot(mock) | ||
| root.SetArgs([]string{"execution", "results", "01ABC"}) | ||
| err := root.Execute() | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "api: connection refused") | ||
| } | ||
|
|
||
| func TestResultsMissingArgument(t *testing.T) { | ||
| root, _ := newTestRoot(&mockClient{}) | ||
| root.SetArgs([]string{"execution", "results"}) | ||
| err := root.Execute() | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "accepts 1 arg(s)") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package execution_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
|
|
||
| "github.com/duneanalytics/cli/cmd/execution" | ||
| "github.com/duneanalytics/cli/cmdutil" | ||
| "github.com/duneanalytics/duneapi-client-go/dune" | ||
| "github.com/duneanalytics/duneapi-client-go/models" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| type mockClient struct { | ||
| dune.DuneClient | ||
| queryResultsV2Fn func(string, models.ResultOptions) (*models.ResultsResponse, error) | ||
| } | ||
|
|
||
| func (m *mockClient) QueryResultsV2(executionID string, options models.ResultOptions) (*models.ResultsResponse, error) { | ||
| return m.queryResultsV2Fn(executionID, options) | ||
| } | ||
|
|
||
| // newTestRoot builds a root → execution command tree with the mock injected. | ||
| func newTestRoot(mock dune.DuneClient) (*cobra.Command, *bytes.Buffer) { | ||
| root := &cobra.Command{ | ||
| Use: "dune", | ||
| PersistentPreRun: func(cmd *cobra.Command, _ []string) { | ||
| cmdutil.SetClient(cmd, mock) | ||
| }, | ||
| } | ||
| root.SetContext(context.Background()) | ||
| root.AddCommand(execution.NewExecutionCmd()) | ||
|
|
||
| var buf bytes.Buffer | ||
| root.SetOut(&buf) | ||
|
|
||
| return root, &buf | ||
| } |


Uh oh!
There was an error while loading. Please reload this page.