From bdf2704d045490fc0e3d90c3345d4b6f6bdfcfbc Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Fri, 10 Apr 2026 13:17:37 +0100 Subject: [PATCH 1/5] Remove time range operators --- pkg/github/schema.go | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/pkg/github/schema.go b/pkg/github/schema.go index c6301bdc..11dde3f7 100644 --- a/pkg/github/schema.go +++ b/pkg/github/schema.go @@ -10,12 +10,6 @@ import ( ) var ( - timeRangeOperators = []schemas.Operator{ - schemas.OperatorGreaterThan, - schemas.OperatorGreaterThanOrEqual, - schemas.OperatorLessThan, - schemas.OperatorLessThanOrEqual, - } equalityOperators = []schemas.Operator{ schemas.OperatorEquals, schemas.OperatorIn, @@ -202,7 +196,7 @@ func getAllTables() []schemas.Table { {Name: "author_login", Type: schemas.ColumnTypeString}, {Name: "author_email", Type: schemas.ColumnTypeString}, {Name: "author_company", Type: schemas.ColumnTypeString}, - {Name: "committed_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "committed_at", Type: schemas.ColumnTypeDatetime}, {Name: "pushed_at", Type: schemas.ColumnTypeDatetime}, {Name: "message", Type: schemas.ColumnTypeString}, }, @@ -218,9 +212,9 @@ func getAllTables() []schemas.Table { {Name: "number", Type: schemas.ColumnTypeInt64}, {Name: "state", Type: schemas.ColumnTypeEnum, Values: []string{"open", "closed"}, Operators: equalityOperators}, {Name: "closed", Type: schemas.ColumnTypeBoolean}, - {Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, - {Name: "closed_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, - {Name: "updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "created_at", Type: schemas.ColumnTypeDatetime}, + {Name: "closed_at", Type: schemas.ColumnTypeDatetime}, + {Name: "updated_at", Type: schemas.ColumnTypeDatetime}, {Name: "labels", Type: schemas.ColumnTypeJSON, Operators: equalityOperators}, {Name: "assignees", Type: schemas.ColumnTypeJSON, Operators: equalityOperators}, {Name: "milestone", Type: schemas.ColumnTypeString, Operators: equalityOperators}, @@ -246,14 +240,14 @@ func getAllTables() []schemas.Table { {Name: "locked", Type: schemas.ColumnTypeBoolean}, {Name: "merged", Type: schemas.ColumnTypeBoolean}, {Name: "mergeable", Type: schemas.ColumnTypeString}, - {Name: "closed_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, - {Name: "merged_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "closed_at", Type: schemas.ColumnTypeDatetime}, + {Name: "merged_at", Type: schemas.ColumnTypeDatetime}, {Name: "merged_by_name", Type: schemas.ColumnTypeString}, {Name: "merged_by_login", Type: schemas.ColumnTypeString}, {Name: "merged_by_email", Type: schemas.ColumnTypeString}, {Name: "merged_by_company", Type: schemas.ColumnTypeString}, - {Name: "updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, - {Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "updated_at", Type: schemas.ColumnTypeDatetime}, + {Name: "created_at", Type: schemas.ColumnTypeDatetime}, {Name: "open_time", Type: schemas.ColumnTypeFloat64}, {Name: "labels", Type: schemas.ColumnTypeJSON, Operators: equalityOperators}, }, @@ -278,8 +272,8 @@ func getAllTables() []schemas.Table { {Name: "review_url", Type: schemas.ColumnTypeString}, {Name: "review_state", Type: schemas.ColumnTypeString}, {Name: "review_comment_count", Type: schemas.ColumnTypeInt64}, - {Name: "review_updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, - {Name: "review_created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "review_updated_at", Type: schemas.ColumnTypeDatetime}, + {Name: "review_created_at", Type: schemas.ColumnTypeDatetime}, }, }, { @@ -318,7 +312,7 @@ func getAllTables() []schemas.Table { {Name: "author_login", Type: schemas.ColumnTypeString}, {Name: "author_email", Type: schemas.ColumnTypeString}, {Name: "author_company", Type: schemas.ColumnTypeString}, - {Name: "date", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "date", Type: schemas.ColumnTypeDatetime}, }, }, { @@ -332,7 +326,7 @@ func getAllTables() []schemas.Table { {Name: "tag", Type: schemas.ColumnTypeString}, {Name: "url", Type: schemas.ColumnTypeString}, {Name: "created_at", Type: schemas.ColumnTypeDatetime}, - {Name: "published_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "published_at", Type: schemas.ColumnTypeDatetime}, }, }, { @@ -408,7 +402,7 @@ func getAllTables() []schemas.Table { Name: normalizeTableNames(models.QueryTypeStargazers), TableParameters: repoScopedTableParameters, Columns: []schemas.Column{ - {Name: "starred_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "starred_at", Type: schemas.ColumnTypeDatetime}, {Name: "star_count", Type: schemas.ColumnTypeInt64}, {Name: "id", Type: schemas.ColumnTypeString}, {Name: "login", Type: schemas.ColumnTypeString}, @@ -426,8 +420,8 @@ func getAllTables() []schemas.Table { {Name: "name", Type: schemas.ColumnTypeString}, {Name: "path", Type: schemas.ColumnTypeString}, {Name: "state", Type: schemas.ColumnTypeString}, - {Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, - {Name: "updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "created_at", Type: schemas.ColumnTypeDatetime}, + {Name: "updated_at", Type: schemas.ColumnTypeDatetime}, {Name: "url", Type: schemas.ColumnTypeString}, {Name: "html_url", Type: schemas.ColumnTypeString}, {Name: "badge_url", Type: schemas.ColumnTypeString}, @@ -466,7 +460,7 @@ func getAllTables() []schemas.Table { {Name: "name", Type: schemas.ColumnTypeString}, {Name: "head_branch", Type: schemas.ColumnTypeString, Operators: []schemas.Operator{schemas.OperatorEquals}}, {Name: "head_sha", Type: schemas.ColumnTypeString}, - {Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "created_at", Type: schemas.ColumnTypeDatetime}, {Name: "updated_at", Type: schemas.ColumnTypeDatetime}, {Name: "run_started_at", Type: schemas.ColumnTypeDatetime}, {Name: "html_url", Type: schemas.ColumnTypeString}, @@ -514,8 +508,8 @@ func getAllTables() []schemas.Table { {Name: "environment", Type: schemas.ColumnTypeString}, {Name: "description", Type: schemas.ColumnTypeString}, {Name: "creator", Type: schemas.ColumnTypeString}, - {Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, - {Name: "updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators}, + {Name: "created_at", Type: schemas.ColumnTypeDatetime}, + {Name: "updated_at", Type: schemas.ColumnTypeDatetime}, {Name: "url", Type: schemas.ColumnTypeString}, {Name: "statuses_url", Type: schemas.ColumnTypeString}, }, From 7cf5aa54ba6f83ab3811f14329e4966d3d232825 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Fri, 10 Apr 2026 13:18:07 +0100 Subject: [PATCH 2/5] Remove unsupported equality operators --- pkg/github/schema.go | 4 ++-- pkg/github/sql.go | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/github/schema.go b/pkg/github/schema.go index 11dde3f7..b9a21a95 100644 --- a/pkg/github/schema.go +++ b/pkg/github/schema.go @@ -379,8 +379,8 @@ func getAllTables() []schemas.Table { {Name: "cvssScore", Type: schemas.ColumnTypeFloat64}, {Name: "cvssVector", Type: schemas.ColumnTypeString}, {Name: "permalink", Type: schemas.ColumnTypeString}, - {Name: "severity", Type: schemas.ColumnTypeString, Operators: equalityOperators}, - {Name: "state", Type: schemas.ColumnTypeString, Operators: equalityOperators}, + {Name: "severity", Type: schemas.ColumnTypeString}, + {Name: "state", Type: schemas.ColumnTypeString}, }, }, { diff --git a/pkg/github/sql.go b/pkg/github/sql.go index 451d6f15..a90497b1 100644 --- a/pkg/github/sql.go +++ b/pkg/github/sql.go @@ -139,8 +139,6 @@ func applyFilters(queryType string, options map[string]interface{}, filters []sc } case models.QueryTypePullRequests, models.QueryTypePullRequestReviews: switch f.Name { - case "state": - appendEqualitySearchQualifier("state", condition.Operator, values, false) case "author_login": appendEqualitySearchQualifier("author", condition.Operator, values, false) case "labels": From 3dd92c46e2efe818554f35a3018b08f3b8ac058a Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Fri, 10 Apr 2026 13:19:18 +0100 Subject: [PATCH 3/5] Add time field support and update tests --- pkg/github/schema.go | 62 +++++++++-- pkg/github/sql.go | 77 ++++++++++++- pkg/github/sql_handler_test.go | 192 ++++++++++++++++++++++++++++++++- 3 files changed, 316 insertions(+), 15 deletions(-) diff --git a/pkg/github/schema.go b/pkg/github/schema.go index b9a21a95..b6cafc1f 100644 --- a/pkg/github/schema.go +++ b/pkg/github/schema.go @@ -23,6 +23,12 @@ var ( {Name: "repository", DependsOn: []string{"organization"}, Required: true}, } + repoScopedWithTimeFieldTableParameters = []schemas.TableParameter{ + {Name: "organization", Root: true, Required: true}, + {Name: "repository", DependsOn: []string{"organization"}, Required: true}, + {Name: "timeField", Root: true}, + } + orgOnlyTableParameters = []schemas.TableParameter{ {Name: "organization", Root: true, Required: true}, } @@ -55,14 +61,31 @@ func (p *SchemaProvider) Schema(ctx context.Context, req *schemas.SchemaRequest) tables := getAllTables() var tableParamValues map[string]map[string][]string - if len(orgRepos.Orgs) > 0 { - tableParamValues = make(map[string]map[string][]string) - for _, t := range tables { - for _, tp := range t.TableParameters { - if tp.Root && tp.Name == "organization" { - tableParamValues[t.Name] = map[string][]string{ - "organization": orgRepos.Orgs, + for _, t := range tables { + for _, tp := range t.TableParameters { + if !tp.Root { + continue + } + switch tp.Name { + case "organization": + if len(orgRepos.Orgs) > 0 { + if tableParamValues == nil { + tableParamValues = make(map[string]map[string][]string) + } + if tableParamValues[t.Name] == nil { + tableParamValues[t.Name] = make(map[string][]string) + } + tableParamValues[t.Name]["organization"] = orgRepos.Orgs + } + case "timeField": + if vals := timeFieldValuesForTable(t.Name); len(vals) > 0 { + if tableParamValues == nil { + tableParamValues = make(map[string]map[string][]string) } + if tableParamValues[t.Name] == nil { + tableParamValues[t.Name] = make(map[string][]string) + } + tableParamValues[t.Name]["timeField"] = vals } } } @@ -135,6 +158,10 @@ func (p *SchemaProvider) TableParameterValues(ctx context.Context, req *schemas. names[i] = r.Name } result[param] = names + case "timeField": + if vals := timeFieldValuesForTable(stripTableParameterValues(req.Table)); len(vals) > 0 { + result[param] = vals + } case "workflow": org := req.DependencyValues["organization"] repo := req.DependencyValues["repository"] @@ -203,7 +230,7 @@ func getAllTables() []schemas.Table { }, { Name: normalizeTableNames(models.QueryTypeIssues), - TableParameters: repoScopedTableParameters, + TableParameters: repoScopedWithTimeFieldTableParameters, Columns: []schemas.Column{ {Name: "title", Type: schemas.ColumnTypeString}, {Name: "author", Type: schemas.ColumnTypeString, Operators: equalityOperators}, @@ -222,7 +249,7 @@ func getAllTables() []schemas.Table { }, { Name: normalizeTableNames(models.QueryTypePullRequests), - TableParameters: repoScopedTableParameters, + TableParameters: repoScopedWithTimeFieldTableParameters, Columns: []schemas.Column{ {Name: "number", Type: schemas.ColumnTypeInt64}, {Name: "title", Type: schemas.ColumnTypeString}, @@ -254,7 +281,7 @@ func getAllTables() []schemas.Table { }, { Name: normalizeTableNames(models.QueryTypePullRequestReviews), - TableParameters: repoScopedTableParameters, + TableParameters: repoScopedWithTimeFieldTableParameters, Columns: []schemas.Column{ {Name: "pull_request_number", Type: schemas.ColumnTypeInt64}, {Name: "pull_request_title", Type: schemas.ColumnTypeString}, @@ -414,7 +441,7 @@ func getAllTables() []schemas.Table { }, { Name: normalizeTableNames(models.QueryTypeWorkflows), - TableParameters: repoScopedTableParameters, + TableParameters: repoScopedWithTimeFieldTableParameters, Columns: []schemas.Column{ {Name: "id", Type: schemas.ColumnTypeInt64}, {Name: "name", Type: schemas.ColumnTypeString}, @@ -530,6 +557,19 @@ func getAllTables() []schemas.Table { } } +func timeFieldValuesForTable(tableName string) []string { + switch tableName { + case normalizeTableNames(models.QueryTypeIssues): + return []string{"created", "closed", "updated"} + case normalizeTableNames(models.QueryTypePullRequests), + normalizeTableNames(models.QueryTypePullRequestReviews): + return []string{"created", "closed", "merged", "updated"} + case normalizeTableNames(models.QueryTypeWorkflows): + return []string{"created", "updated"} + } + return nil +} + func normalizeTableNames(table string) string { return strings.ToLower(strings.ReplaceAll(table, "_", "-")) } diff --git a/pkg/github/sql.go b/pkg/github/sql.go index a90497b1..8655de4a 100644 --- a/pkg/github/sql.go +++ b/pkg/github/sql.go @@ -203,8 +203,27 @@ func applyFilters(queryType string, options map[string]interface{}, filters []sc } } case models.QueryTypeRepositories: - if f.Name == "name" && (condition.Operator == schemas.OperatorLike || condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn) { - options["repository"] = values[0] + appendRepoSearchQualifier := func(qualifier string) { + existing, _ := options["repository"].(string) + options["repository"] = strings.TrimSpace(existing + " " + qualifier) + } + switch f.Name { + case "name": + if condition.Operator == schemas.OperatorLike || condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn { + appendRepoSearchQualifier(values[0]) + } + case "is_fork": + if condition.Operator == schemas.OperatorEquals && values[0] == "true" { + appendRepoSearchQualifier("fork:only") + } + case "is_private": + if condition.Operator == schemas.OperatorEquals { + if values[0] == "true" { + appendRepoSearchQualifier("is:private") + } else { + appendRepoSearchQualifier("is:public") + } + } } } } @@ -223,6 +242,48 @@ func applyFilters(queryType string, options map[string]interface{}, filters []sc return searchQualifiers } +func resolveTimeField(queryType, value string) (int, bool) { + switch queryType { + case models.QueryTypeIssues: + switch value { + case "created": + return int(models.IssueCreatedAt), true + case "closed": + return int(models.IssueClosedAt), true + case "updated": + return int(models.IssueUpdatedAt), true + } + case models.QueryTypePullRequests, models.QueryTypePullRequestReviews: + switch value { + case "closed": + return int(models.PullRequestClosedAt), true + case "created": + return int(models.PullRequestCreatedAt), true + case "merged": + return int(models.PullRequestMergedAt), true + case "updated": + return int(models.PullRequestUpdatedAt), true + } + case models.QueryTypeWorkflows: + switch value { + case "created": + return int(models.WorkflowCreatedAt), true + case "updated": + return int(models.WorkflowUpdatedAt), true + } + } + return 0, false +} + +func defaultTimeField(queryType string) int { + switch queryType { + case models.QueryTypePullRequests, models.QueryTypePullRequestReviews: + return int(models.PullRequestCreatedAt) + default: + return 0 + } +} + func normalizeGrafanaSQLRequest(req *backend.QueryDataRequest) *backend.QueryDataRequest { if req == nil || len(req.Queries) == 0 { return req @@ -294,6 +355,18 @@ func normalizeGrafanaSQLRequest(req *backend.QueryDataRequest) *backend.QueryDat opts["workflow"] = v } + switch queryType { + case models.QueryTypeIssues, models.QueryTypePullRequests, models.QueryTypePullRequestReviews, models.QueryTypeWorkflows: + opts, _ := normalized["options"].(map[string]interface{}) + if tfStr := strings.TrimSpace(anyToString(query.TableParameterValues["timeField"])); tfStr != "" { + if tf, ok := resolveTimeField(queryType, tfStr); ok { + opts["timeField"] = tf + } + } else { + opts["timeField"] = defaultTimeField(queryType) + } + } + if len(query.Filters) > 0 { applyFilters(queryType, normalized, query.Filters) } diff --git a/pkg/github/sql_handler_test.go b/pkg/github/sql_handler_test.go index e8163f97..a239f3e8 100644 --- a/pkg/github/sql_handler_test.go +++ b/pkg/github/sql_handler_test.go @@ -28,8 +28,8 @@ func TestTableToQueryTypeCoversAllTypes(t *testing.T) { models.QueryTypeProjects, models.QueryTypeStargazers, models.QueryTypeWorkflows, models.QueryTypeWorkflowUsage, models.QueryTypeWorkflowRuns, - models.QueryTypeCodeScanning, models.QueryTypeOrganizations, - models.QueryTypeGraphQL, + models.QueryTypeCodeScanning, models.QueryTypeDeployments, + models.QueryTypeOrganizations, models.QueryTypeGraphQL, } for _, qt := range allQueryTypes { tableName := normalizeTableNames(qt) @@ -64,6 +64,7 @@ func TestNormalizeAllTableTypes(t *testing.T) { {name: "tags", table: "tags_grafana_grafana", wantType: models.QueryTypeTags, wantOwner: "grafana", wantRepo: "grafana"}, {name: "releases", table: "releases_grafana_grafana", wantType: models.QueryTypeReleases, wantOwner: "grafana", wantRepo: "grafana"}, {name: "workflows", table: "workflows_grafana_grafana", wantType: models.QueryTypeWorkflows, wantOwner: "grafana", wantRepo: "grafana"}, + {name: "deployments", table: "deployments_grafana_grafana", wantType: models.QueryTypeDeployments, wantOwner: "grafana", wantRepo: "grafana"}, {name: "repositories owner only", table: "repositories_grafana", wantType: models.QueryTypeRepositories, wantOwner: "grafana", wantRepo: ""}, {name: "projects owner only", table: "projects_grafana", wantType: models.QueryTypeProjects, wantOwner: "grafana", wantRepo: ""}, {name: "organizations no table parameters", table: "organizations", wantType: models.QueryTypeOrganizations, wantOwner: "", wantRepo: ""}, @@ -672,6 +673,63 @@ func TestNormalizeGrafanaSQLRequestWithFilters(t *testing.T) { } }) + t.Run("pushes down is_fork filter for repositories", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"repositories_grafana","filters":[{"name":"is_fork","conditions":[{"operator":"=","value":"true"}]}]}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + repo, _ := raw["repository"].(string) + if repo != "fork:only" { + t.Errorf("expected repository 'fork:only', got %q", repo) + } + }) + + t.Run("pushes down is_private filter for repositories", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"repositories_grafana","filters":[{"name":"is_private","conditions":[{"operator":"=","value":"true"}]}]}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + repo, _ := raw["repository"].(string) + if repo != "is:private" { + t.Errorf("expected repository 'is:private', got %q", repo) + } + }) + + t.Run("pushes down is_private false as is:public", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"repositories_grafana","filters":[{"name":"is_private","conditions":[{"operator":"=","value":"false"}]}]}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{ + {RefID: "A", JSON: queryJSON}, + }, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + repo, _ := raw["repository"].(string) + if repo != "is:public" { + t.Errorf("expected repository 'is:public', got %q", repo) + } + }) + t.Run("pushes down tool_name filter for code-scanning", func(t *testing.T) { queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"code-scanning_grafana_grafana","filters":[{"name":"tool_name","conditions":[{"operator":"=","value":"CodeQL"}]}]}`) req := &backend.QueryDataRequest{ @@ -805,3 +863,133 @@ func TestNormalizeGrafanaSQLRequestWithoutFeatureToggle(t *testing.T) { } }) } + +func TestTimeFieldDefaults(t *testing.T) { + t.Run("issues default timeField is created (0)", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues_grafana_grafana"}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{{RefID: "A", JSON: queryJSON}}, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts := raw["options"].(map[string]interface{}) + tf, ok := opts["timeField"].(float64) + if !ok { + t.Fatal("expected timeField to be set") + } + if int(tf) != int(models.IssueCreatedAt) { + t.Errorf("expected timeField %d (IssueCreatedAt), got %d", models.IssueCreatedAt, int(tf)) + } + }) + + t.Run("pull-requests default timeField is created (1)", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"pull-requests_grafana_grafana"}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{{RefID: "A", JSON: queryJSON}}, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts := raw["options"].(map[string]interface{}) + tf := int(opts["timeField"].(float64)) + if tf != int(models.PullRequestCreatedAt) { + t.Errorf("expected timeField %d (PullRequestCreatedAt), got %d", models.PullRequestCreatedAt, tf) + } + }) + + t.Run("workflows default timeField is none (0)", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"workflows_grafana_grafana"}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{{RefID: "A", JSON: queryJSON}}, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts := raw["options"].(map[string]interface{}) + tf := int(opts["timeField"].(float64)) + if tf != int(models.WorkflowTimeFieldNone) { + t.Errorf("expected timeField %d (WorkflowTimeFieldNone), got %d", models.WorkflowTimeFieldNone, tf) + } + }) + + t.Run("timeField table parameter overrides default for PRs", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"pull-requests","tableParameterValues":{"organization":"grafana","repository":"grafana","timeField":"merged"}}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{{RefID: "A", JSON: queryJSON}}, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts := raw["options"].(map[string]interface{}) + tf := int(opts["timeField"].(float64)) + if tf != int(models.PullRequestMergedAt) { + t.Errorf("expected timeField %d (PullRequestMergedAt), got %d", models.PullRequestMergedAt, tf) + } + }) + + t.Run("timeField table parameter sets closed for issues", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"issues","tableParameterValues":{"organization":"grafana","repository":"grafana","timeField":"closed"}}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{{RefID: "A", JSON: queryJSON}}, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts := raw["options"].(map[string]interface{}) + tf := int(opts["timeField"].(float64)) + if tf != int(models.IssueClosedAt) { + t.Errorf("expected timeField %d (IssueClosedAt), got %d", models.IssueClosedAt, tf) + } + }) + + t.Run("timeField table parameter sets updated for workflows", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"workflows","tableParameterValues":{"organization":"grafana","repository":"grafana","timeField":"updated"}}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{{RefID: "A", JSON: queryJSON}}, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts := raw["options"].(map[string]interface{}) + tf := int(opts["timeField"].(float64)) + if tf != int(models.WorkflowUpdatedAt) { + t.Errorf("expected timeField %d (WorkflowUpdatedAt), got %d", models.WorkflowUpdatedAt, tf) + } + }) + + t.Run("commits does not get timeField set", func(t *testing.T) { + queryJSON := []byte(`{"refId":"A","grafanaSql":true,"table":"commits_grafana_grafana"}`) + req := &backend.QueryDataRequest{ + PluginContext: pluginCtxWithFeatureToggle(), + Queries: []backend.DataQuery{{RefID: "A", JSON: queryJSON}}, + } + out := normalizeGrafanaSQLRequest(req) + var raw map[string]interface{} + if err := json.Unmarshal(out.Queries[0].JSON, &raw); err != nil { + t.Fatal(err) + } + opts := raw["options"].(map[string]interface{}) + if _, exists := opts["timeField"]; exists { + t.Error("expected no timeField for commits") + } + }) +} From f94e7534caa62fe27e141c508abb6a76aa7e9da9 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Fri, 10 Apr 2026 13:35:59 +0100 Subject: [PATCH 4/5] Minor fix --- pkg/github/sql.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/github/sql.go b/pkg/github/sql.go index 8655de4a..e6b22059 100644 --- a/pkg/github/sql.go +++ b/pkg/github/sql.go @@ -361,6 +361,8 @@ func normalizeGrafanaSQLRequest(req *backend.QueryDataRequest) *backend.QueryDat if tfStr := strings.TrimSpace(anyToString(query.TableParameterValues["timeField"])); tfStr != "" { if tf, ok := resolveTimeField(queryType, tfStr); ok { opts["timeField"] = tf + } else { + opts["timeField"] = defaultTimeField(queryType) } } else { opts["timeField"] = defaultTimeField(queryType) From d4a88dde74fb5b2fe3dc53cc42ff3faebef883cb Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Fri, 10 Apr 2026 16:09:16 +0100 Subject: [PATCH 5/5] Review --- pkg/github/sql.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/github/sql.go b/pkg/github/sql.go index e6b22059..3fbdacfc 100644 --- a/pkg/github/sql.go +++ b/pkg/github/sql.go @@ -242,34 +242,34 @@ func applyFilters(queryType string, options map[string]interface{}, filters []sc return searchQualifiers } -func resolveTimeField(queryType, value string) (int, bool) { +func resolveTimeField(queryType, value string) (any, bool) { switch queryType { case models.QueryTypeIssues: switch value { case "created": - return int(models.IssueCreatedAt), true + return models.IssueCreatedAt, true case "closed": - return int(models.IssueClosedAt), true + return models.IssueClosedAt, true case "updated": - return int(models.IssueUpdatedAt), true + return models.IssueUpdatedAt, true } case models.QueryTypePullRequests, models.QueryTypePullRequestReviews: switch value { case "closed": - return int(models.PullRequestClosedAt), true + return models.PullRequestClosedAt, true case "created": - return int(models.PullRequestCreatedAt), true + return models.PullRequestCreatedAt, true case "merged": - return int(models.PullRequestMergedAt), true + return models.PullRequestMergedAt, true case "updated": - return int(models.PullRequestUpdatedAt), true + return models.PullRequestUpdatedAt, true } case models.QueryTypeWorkflows: switch value { case "created": - return int(models.WorkflowCreatedAt), true + return models.WorkflowCreatedAt, true case "updated": - return int(models.WorkflowUpdatedAt), true + return models.WorkflowUpdatedAt, true } } return 0, false