Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 71 additions & 37 deletions pkg/github/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ import (
)

var (
timeRangeOperators = []schemas.Operator{
schemas.OperatorGreaterThan,
schemas.OperatorGreaterThanOrEqual,
schemas.OperatorLessThan,
schemas.OperatorLessThanOrEqual,
}
equalityOperators = []schemas.Operator{
schemas.OperatorEquals,
schemas.OperatorIn,
Expand All @@ -29,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},
}
Expand Down Expand Up @@ -61,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
}
}
}
Expand Down Expand Up @@ -141,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"]
Expand Down Expand Up @@ -202,14 +223,14 @@ 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},
},
},
{
Name: normalizeTableNames(models.QueryTypeIssues),
TableParameters: repoScopedTableParameters,
TableParameters: repoScopedWithTimeFieldTableParameters,
Columns: []schemas.Column{
{Name: "title", Type: schemas.ColumnTypeString},
{Name: "author", Type: schemas.ColumnTypeString, Operators: equalityOperators},
Expand All @@ -218,17 +239,17 @@ 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},
},
},
{
Name: normalizeTableNames(models.QueryTypePullRequests),
TableParameters: repoScopedTableParameters,
TableParameters: repoScopedWithTimeFieldTableParameters,
Columns: []schemas.Column{
{Name: "number", Type: schemas.ColumnTypeInt64},
{Name: "title", Type: schemas.ColumnTypeString},
Expand All @@ -246,21 +267,21 @@ 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},
},
},
{
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},
Expand All @@ -278,8 +299,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},
},
},
{
Expand Down Expand Up @@ -318,7 +339,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},
},
},
{
Expand All @@ -332,7 +353,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},
},
},
{
Expand Down Expand Up @@ -385,8 +406,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},
},
},
{
Expand All @@ -408,7 +429,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},
Expand All @@ -420,14 +441,14 @@ 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},
{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},
Expand Down Expand Up @@ -466,7 +487,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},
Expand Down Expand Up @@ -514,8 +535,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},
},
Expand All @@ -536,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, "_", "-"))
}
81 changes: 77 additions & 4 deletions pkg/github/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -205,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")
}
}
}
}
}
Expand All @@ -225,6 +242,48 @@ func applyFilters(queryType string, options map[string]interface{}, filters []sc
return searchQualifiers
}

func resolveTimeField(queryType, value string) (any, bool) {
switch queryType {
case models.QueryTypeIssues:
switch value {
case "created":
return models.IssueCreatedAt, true
case "closed":
return models.IssueClosedAt, true
case "updated":
return models.IssueUpdatedAt, true
}
case models.QueryTypePullRequests, models.QueryTypePullRequestReviews:
switch value {
case "closed":
return models.PullRequestClosedAt, true
case "created":
return models.PullRequestCreatedAt, true
case "merged":
return models.PullRequestMergedAt, true
case "updated":
return models.PullRequestUpdatedAt, true
}
case models.QueryTypeWorkflows:
switch value {
case "created":
return models.WorkflowCreatedAt, true
case "updated":
return 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
Expand Down Expand Up @@ -296,6 +355,20 @@ 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)
}
} else {
opts["timeField"] = defaultTimeField(queryType)
}
Comment on lines +358 to +369
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If tableParameterValues.timeField is provided but resolveTimeField returns ok=false (invalid/unsupported value), the code currently leaves options.timeField unset. That will cause the downstream JSON unmarshal defaults to kick in (e.g., PRs default to PullRequestClosedAt because enum 0 != the intended default), which is surprising and can change query semantics. Consider falling back to defaultTimeField(queryType) when the provided value is non-empty but unrecognized (and optionally normalizing case with strings.ToLower).

Copilot uses AI. Check for mistakes.
}

if len(query.Filters) > 0 {
applyFilters(queryType, normalized, query.Filters)
}
Expand Down
Loading
Loading