Skip to content

Commit b2f4336

Browse files
authored
Schema updates (#722)
## Summary Cleans up the `schemads` schema declarations, adds new filter pushdown and `timeField` table parameter support, and expands test coverage. ### Schema cleanup (`schema.go`) - **Removed `timeRangeOperators`** — datetime columns no longer declare explicit `>`, `>=`, `<`, `<=` operators for filter pushdown. Time-based filtering is handled by Grafana's global `TimeRange` mechanism, so advertising these operators was misleading. - **Removed `equalityOperators` from Vulnerabilities `severity`/`state`** — these columns aren't backed by any pushdown implementation in `applyFilters` and the GitHub API doesn't support filtering them server-side. ### Filter pushdown for Repositories (`sql.go`) - Added pushdown support for `is_fork` and `is_private` filters on the Repositories table, translating them into GitHub search qualifiers (`fork:only`, `is:private`, `is:public`). - Added `QueryTypeDeployments` to the `tableToQueryType` map. ### `timeField` table parameter (`schema.go`, `sql.go`) - Introduced `repoScopedWithTimeFieldTableParameters` — extends the standard repo-scoped parameters with an optional `timeField` parameter. - Applied to **Issues**, **Pull Requests**, **PR Reviews**, and **Workflows** tables. - Added `timeFieldValuesForTable` helper providing valid values per table type (e.g. `created`, `closed`, `merged`, `updated`). - Updated `Schema` and `TableParameterValues` handlers to serve `timeField` values dynamically. - Added `resolveTimeField` / `defaultTimeField` helpers in `sql.go` to map user-provided strings (e.g. `"merged"`) to the correct integer enum constant, with sensible defaults per query type. - Integrated into `normalizeGrafanaSQLRequest` so the time range picker filters by the user-selected datetime column.
1 parent 0c20fba commit b2f4336

3 files changed

Lines changed: 338 additions & 43 deletions

File tree

pkg/github/schema.go

Lines changed: 71 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ import (
1010
)
1111

1212
var (
13-
timeRangeOperators = []schemas.Operator{
14-
schemas.OperatorGreaterThan,
15-
schemas.OperatorGreaterThanOrEqual,
16-
schemas.OperatorLessThan,
17-
schemas.OperatorLessThanOrEqual,
18-
}
1913
equalityOperators = []schemas.Operator{
2014
schemas.OperatorEquals,
2115
schemas.OperatorIn,
@@ -29,6 +23,12 @@ var (
2923
{Name: "repository", DependsOn: []string{"organization"}, Required: true},
3024
}
3125

26+
repoScopedWithTimeFieldTableParameters = []schemas.TableParameter{
27+
{Name: "organization", Root: true, Required: true},
28+
{Name: "repository", DependsOn: []string{"organization"}, Required: true},
29+
{Name: "timeField", Root: true},
30+
}
31+
3232
orgOnlyTableParameters = []schemas.TableParameter{
3333
{Name: "organization", Root: true, Required: true},
3434
}
@@ -61,14 +61,31 @@ func (p *SchemaProvider) Schema(ctx context.Context, req *schemas.SchemaRequest)
6161
tables := getAllTables()
6262

6363
var tableParamValues map[string]map[string][]string
64-
if len(orgRepos.Orgs) > 0 {
65-
tableParamValues = make(map[string]map[string][]string)
66-
for _, t := range tables {
67-
for _, tp := range t.TableParameters {
68-
if tp.Root && tp.Name == "organization" {
69-
tableParamValues[t.Name] = map[string][]string{
70-
"organization": orgRepos.Orgs,
64+
for _, t := range tables {
65+
for _, tp := range t.TableParameters {
66+
if !tp.Root {
67+
continue
68+
}
69+
switch tp.Name {
70+
case "organization":
71+
if len(orgRepos.Orgs) > 0 {
72+
if tableParamValues == nil {
73+
tableParamValues = make(map[string]map[string][]string)
74+
}
75+
if tableParamValues[t.Name] == nil {
76+
tableParamValues[t.Name] = make(map[string][]string)
7177
}
78+
tableParamValues[t.Name]["organization"] = orgRepos.Orgs
79+
}
80+
case "timeField":
81+
if vals := timeFieldValuesForTable(t.Name); len(vals) > 0 {
82+
if tableParamValues == nil {
83+
tableParamValues = make(map[string]map[string][]string)
84+
}
85+
if tableParamValues[t.Name] == nil {
86+
tableParamValues[t.Name] = make(map[string][]string)
87+
}
88+
tableParamValues[t.Name]["timeField"] = vals
7289
}
7390
}
7491
}
@@ -141,6 +158,10 @@ func (p *SchemaProvider) TableParameterValues(ctx context.Context, req *schemas.
141158
names[i] = r.Name
142159
}
143160
result[param] = names
161+
case "timeField":
162+
if vals := timeFieldValuesForTable(stripTableParameterValues(req.Table)); len(vals) > 0 {
163+
result[param] = vals
164+
}
144165
case "workflow":
145166
org := req.DependencyValues["organization"]
146167
repo := req.DependencyValues["repository"]
@@ -202,14 +223,14 @@ func getAllTables() []schemas.Table {
202223
{Name: "author_login", Type: schemas.ColumnTypeString},
203224
{Name: "author_email", Type: schemas.ColumnTypeString},
204225
{Name: "author_company", Type: schemas.ColumnTypeString},
205-
{Name: "committed_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
226+
{Name: "committed_at", Type: schemas.ColumnTypeDatetime},
206227
{Name: "pushed_at", Type: schemas.ColumnTypeDatetime},
207228
{Name: "message", Type: schemas.ColumnTypeString},
208229
},
209230
},
210231
{
211232
Name: normalizeTableNames(models.QueryTypeIssues),
212-
TableParameters: repoScopedTableParameters,
233+
TableParameters: repoScopedWithTimeFieldTableParameters,
213234
Columns: []schemas.Column{
214235
{Name: "title", Type: schemas.ColumnTypeString},
215236
{Name: "author", Type: schemas.ColumnTypeString, Operators: equalityOperators},
@@ -218,17 +239,17 @@ func getAllTables() []schemas.Table {
218239
{Name: "number", Type: schemas.ColumnTypeInt64},
219240
{Name: "state", Type: schemas.ColumnTypeEnum, Values: []string{"open", "closed"}, Operators: equalityOperators},
220241
{Name: "closed", Type: schemas.ColumnTypeBoolean},
221-
{Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
222-
{Name: "closed_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
223-
{Name: "updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
242+
{Name: "created_at", Type: schemas.ColumnTypeDatetime},
243+
{Name: "closed_at", Type: schemas.ColumnTypeDatetime},
244+
{Name: "updated_at", Type: schemas.ColumnTypeDatetime},
224245
{Name: "labels", Type: schemas.ColumnTypeJSON, Operators: equalityOperators},
225246
{Name: "assignees", Type: schemas.ColumnTypeJSON, Operators: equalityOperators},
226247
{Name: "milestone", Type: schemas.ColumnTypeString, Operators: equalityOperators},
227248
},
228249
},
229250
{
230251
Name: normalizeTableNames(models.QueryTypePullRequests),
231-
TableParameters: repoScopedTableParameters,
252+
TableParameters: repoScopedWithTimeFieldTableParameters,
232253
Columns: []schemas.Column{
233254
{Name: "number", Type: schemas.ColumnTypeInt64},
234255
{Name: "title", Type: schemas.ColumnTypeString},
@@ -246,21 +267,21 @@ func getAllTables() []schemas.Table {
246267
{Name: "locked", Type: schemas.ColumnTypeBoolean},
247268
{Name: "merged", Type: schemas.ColumnTypeBoolean},
248269
{Name: "mergeable", Type: schemas.ColumnTypeString},
249-
{Name: "closed_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
250-
{Name: "merged_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
270+
{Name: "closed_at", Type: schemas.ColumnTypeDatetime},
271+
{Name: "merged_at", Type: schemas.ColumnTypeDatetime},
251272
{Name: "merged_by_name", Type: schemas.ColumnTypeString},
252273
{Name: "merged_by_login", Type: schemas.ColumnTypeString},
253274
{Name: "merged_by_email", Type: schemas.ColumnTypeString},
254275
{Name: "merged_by_company", Type: schemas.ColumnTypeString},
255-
{Name: "updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
256-
{Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
276+
{Name: "updated_at", Type: schemas.ColumnTypeDatetime},
277+
{Name: "created_at", Type: schemas.ColumnTypeDatetime},
257278
{Name: "open_time", Type: schemas.ColumnTypeFloat64},
258279
{Name: "labels", Type: schemas.ColumnTypeJSON, Operators: equalityOperators},
259280
},
260281
},
261282
{
262283
Name: normalizeTableNames(models.QueryTypePullRequestReviews),
263-
TableParameters: repoScopedTableParameters,
284+
TableParameters: repoScopedWithTimeFieldTableParameters,
264285
Columns: []schemas.Column{
265286
{Name: "pull_request_number", Type: schemas.ColumnTypeInt64},
266287
{Name: "pull_request_title", Type: schemas.ColumnTypeString},
@@ -278,8 +299,8 @@ func getAllTables() []schemas.Table {
278299
{Name: "review_url", Type: schemas.ColumnTypeString},
279300
{Name: "review_state", Type: schemas.ColumnTypeString},
280301
{Name: "review_comment_count", Type: schemas.ColumnTypeInt64},
281-
{Name: "review_updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
282-
{Name: "review_created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
302+
{Name: "review_updated_at", Type: schemas.ColumnTypeDatetime},
303+
{Name: "review_created_at", Type: schemas.ColumnTypeDatetime},
283304
},
284305
},
285306
{
@@ -318,7 +339,7 @@ func getAllTables() []schemas.Table {
318339
{Name: "author_login", Type: schemas.ColumnTypeString},
319340
{Name: "author_email", Type: schemas.ColumnTypeString},
320341
{Name: "author_company", Type: schemas.ColumnTypeString},
321-
{Name: "date", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
342+
{Name: "date", Type: schemas.ColumnTypeDatetime},
322343
},
323344
},
324345
{
@@ -332,7 +353,7 @@ func getAllTables() []schemas.Table {
332353
{Name: "tag", Type: schemas.ColumnTypeString},
333354
{Name: "url", Type: schemas.ColumnTypeString},
334355
{Name: "created_at", Type: schemas.ColumnTypeDatetime},
335-
{Name: "published_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
356+
{Name: "published_at", Type: schemas.ColumnTypeDatetime},
336357
},
337358
},
338359
{
@@ -385,8 +406,8 @@ func getAllTables() []schemas.Table {
385406
{Name: "cvssScore", Type: schemas.ColumnTypeFloat64},
386407
{Name: "cvssVector", Type: schemas.ColumnTypeString},
387408
{Name: "permalink", Type: schemas.ColumnTypeString},
388-
{Name: "severity", Type: schemas.ColumnTypeString, Operators: equalityOperators},
389-
{Name: "state", Type: schemas.ColumnTypeString, Operators: equalityOperators},
409+
{Name: "severity", Type: schemas.ColumnTypeString},
410+
{Name: "state", Type: schemas.ColumnTypeString},
390411
},
391412
},
392413
{
@@ -408,7 +429,7 @@ func getAllTables() []schemas.Table {
408429
Name: normalizeTableNames(models.QueryTypeStargazers),
409430
TableParameters: repoScopedTableParameters,
410431
Columns: []schemas.Column{
411-
{Name: "starred_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
432+
{Name: "starred_at", Type: schemas.ColumnTypeDatetime},
412433
{Name: "star_count", Type: schemas.ColumnTypeInt64},
413434
{Name: "id", Type: schemas.ColumnTypeString},
414435
{Name: "login", Type: schemas.ColumnTypeString},
@@ -420,14 +441,14 @@ func getAllTables() []schemas.Table {
420441
},
421442
{
422443
Name: normalizeTableNames(models.QueryTypeWorkflows),
423-
TableParameters: repoScopedTableParameters,
444+
TableParameters: repoScopedWithTimeFieldTableParameters,
424445
Columns: []schemas.Column{
425446
{Name: "id", Type: schemas.ColumnTypeInt64},
426447
{Name: "name", Type: schemas.ColumnTypeString},
427448
{Name: "path", Type: schemas.ColumnTypeString},
428449
{Name: "state", Type: schemas.ColumnTypeString},
429-
{Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
430-
{Name: "updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
450+
{Name: "created_at", Type: schemas.ColumnTypeDatetime},
451+
{Name: "updated_at", Type: schemas.ColumnTypeDatetime},
431452
{Name: "url", Type: schemas.ColumnTypeString},
432453
{Name: "html_url", Type: schemas.ColumnTypeString},
433454
{Name: "badge_url", Type: schemas.ColumnTypeString},
@@ -466,7 +487,7 @@ func getAllTables() []schemas.Table {
466487
{Name: "name", Type: schemas.ColumnTypeString},
467488
{Name: "head_branch", Type: schemas.ColumnTypeString, Operators: []schemas.Operator{schemas.OperatorEquals}},
468489
{Name: "head_sha", Type: schemas.ColumnTypeString},
469-
{Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
490+
{Name: "created_at", Type: schemas.ColumnTypeDatetime},
470491
{Name: "updated_at", Type: schemas.ColumnTypeDatetime},
471492
{Name: "run_started_at", Type: schemas.ColumnTypeDatetime},
472493
{Name: "html_url", Type: schemas.ColumnTypeString},
@@ -514,8 +535,8 @@ func getAllTables() []schemas.Table {
514535
{Name: "environment", Type: schemas.ColumnTypeString},
515536
{Name: "description", Type: schemas.ColumnTypeString},
516537
{Name: "creator", Type: schemas.ColumnTypeString},
517-
{Name: "created_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
518-
{Name: "updated_at", Type: schemas.ColumnTypeDatetime, Operators: timeRangeOperators},
538+
{Name: "created_at", Type: schemas.ColumnTypeDatetime},
539+
{Name: "updated_at", Type: schemas.ColumnTypeDatetime},
519540
{Name: "url", Type: schemas.ColumnTypeString},
520541
{Name: "statuses_url", Type: schemas.ColumnTypeString},
521542
},
@@ -536,6 +557,19 @@ func getAllTables() []schemas.Table {
536557
}
537558
}
538559

560+
func timeFieldValuesForTable(tableName string) []string {
561+
switch tableName {
562+
case normalizeTableNames(models.QueryTypeIssues):
563+
return []string{"created", "closed", "updated"}
564+
case normalizeTableNames(models.QueryTypePullRequests),
565+
normalizeTableNames(models.QueryTypePullRequestReviews):
566+
return []string{"created", "closed", "merged", "updated"}
567+
case normalizeTableNames(models.QueryTypeWorkflows):
568+
return []string{"created", "updated"}
569+
}
570+
return nil
571+
}
572+
539573
func normalizeTableNames(table string) string {
540574
return strings.ToLower(strings.ReplaceAll(table, "_", "-"))
541575
}

pkg/github/sql.go

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,6 @@ func applyFilters(queryType string, options map[string]interface{}, filters []sc
139139
}
140140
case models.QueryTypePullRequests, models.QueryTypePullRequestReviews:
141141
switch f.Name {
142-
case "state":
143-
appendEqualitySearchQualifier("state", condition.Operator, values, false)
144142
case "author_login":
145143
appendEqualitySearchQualifier("author", condition.Operator, values, false)
146144
case "labels":
@@ -205,8 +203,27 @@ func applyFilters(queryType string, options map[string]interface{}, filters []sc
205203
}
206204
}
207205
case models.QueryTypeRepositories:
208-
if f.Name == "name" && (condition.Operator == schemas.OperatorLike || condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn) {
209-
options["repository"] = values[0]
206+
appendRepoSearchQualifier := func(qualifier string) {
207+
existing, _ := options["repository"].(string)
208+
options["repository"] = strings.TrimSpace(existing + " " + qualifier)
209+
}
210+
switch f.Name {
211+
case "name":
212+
if condition.Operator == schemas.OperatorLike || condition.Operator == schemas.OperatorEquals || condition.Operator == schemas.OperatorIn {
213+
appendRepoSearchQualifier(values[0])
214+
}
215+
case "is_fork":
216+
if condition.Operator == schemas.OperatorEquals && values[0] == "true" {
217+
appendRepoSearchQualifier("fork:only")
218+
}
219+
case "is_private":
220+
if condition.Operator == schemas.OperatorEquals {
221+
if values[0] == "true" {
222+
appendRepoSearchQualifier("is:private")
223+
} else {
224+
appendRepoSearchQualifier("is:public")
225+
}
226+
}
210227
}
211228
}
212229
}
@@ -225,6 +242,48 @@ func applyFilters(queryType string, options map[string]interface{}, filters []sc
225242
return searchQualifiers
226243
}
227244

245+
func resolveTimeField(queryType, value string) (any, bool) {
246+
switch queryType {
247+
case models.QueryTypeIssues:
248+
switch value {
249+
case "created":
250+
return models.IssueCreatedAt, true
251+
case "closed":
252+
return models.IssueClosedAt, true
253+
case "updated":
254+
return models.IssueUpdatedAt, true
255+
}
256+
case models.QueryTypePullRequests, models.QueryTypePullRequestReviews:
257+
switch value {
258+
case "closed":
259+
return models.PullRequestClosedAt, true
260+
case "created":
261+
return models.PullRequestCreatedAt, true
262+
case "merged":
263+
return models.PullRequestMergedAt, true
264+
case "updated":
265+
return models.PullRequestUpdatedAt, true
266+
}
267+
case models.QueryTypeWorkflows:
268+
switch value {
269+
case "created":
270+
return models.WorkflowCreatedAt, true
271+
case "updated":
272+
return models.WorkflowUpdatedAt, true
273+
}
274+
}
275+
return 0, false
276+
}
277+
278+
func defaultTimeField(queryType string) int {
279+
switch queryType {
280+
case models.QueryTypePullRequests, models.QueryTypePullRequestReviews:
281+
return int(models.PullRequestCreatedAt)
282+
default:
283+
return 0
284+
}
285+
}
286+
228287
func normalizeGrafanaSQLRequest(req *backend.QueryDataRequest) *backend.QueryDataRequest {
229288
if req == nil || len(req.Queries) == 0 {
230289
return req
@@ -296,6 +355,20 @@ func normalizeGrafanaSQLRequest(req *backend.QueryDataRequest) *backend.QueryDat
296355
opts["workflow"] = v
297356
}
298357

358+
switch queryType {
359+
case models.QueryTypeIssues, models.QueryTypePullRequests, models.QueryTypePullRequestReviews, models.QueryTypeWorkflows:
360+
opts, _ := normalized["options"].(map[string]interface{})
361+
if tfStr := strings.TrimSpace(anyToString(query.TableParameterValues["timeField"])); tfStr != "" {
362+
if tf, ok := resolveTimeField(queryType, tfStr); ok {
363+
opts["timeField"] = tf
364+
} else {
365+
opts["timeField"] = defaultTimeField(queryType)
366+
}
367+
} else {
368+
opts["timeField"] = defaultTimeField(queryType)
369+
}
370+
}
371+
299372
if len(query.Filters) > 0 {
300373
applyFilters(queryType, normalized, query.Filters)
301374
}

0 commit comments

Comments
 (0)