Skip to content

Commit

Permalink
override: use static query for override search (#3317)
Browse files Browse the repository at this point in the history
* use static query for override search

* remove debug code
  • Loading branch information
mastercactapus authored Oct 4, 2023
1 parent 762fe55 commit 3319556
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 113 deletions.
115 changes: 115 additions & 0 deletions gadb/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion graphql2/graphqlapp/useroverride.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func (q *Query) UserOverrides(ctx context.Context, input *graphql2.UserOverrideS
}

searchOpts.Limit++
overrides, err := q.OverrideStore.Search(ctx, &searchOpts)
overrides, err := q.OverrideStore.Search(ctx, q.DB, &searchOpts)
if err != nil {
return nil, err
}
Expand Down
55 changes: 55 additions & 0 deletions override/queries.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- name: OverrideSearch :many
WITH AFTER AS (
SELECT
id,
start_time,
end_time
FROM
user_overrides
WHERE
id = sqlc.narg(after_id)::uuid
)
SELECT
o.id,
o.start_time,
o.end_time,
add_user_id,
remove_user_id,
tgt_schedule_id
FROM
user_overrides o
LEFT JOIN AFTER ON TRUE
WHERE (@omit::uuid[] ISNULL
OR o.id <> ALL (@omit))
AND (sqlc.narg(schedule_id)::uuid ISNULL
OR o.tgt_schedule_id = @schedule_id)
AND (@any_user_id::uuid[] ISNULL
OR add_user_id = ANY (@any_user_id::uuid[])
OR remove_user_id = ANY (@any_user_id::uuid[]))
AND (@add_user_id::uuid[] ISNULL
OR add_user_id = ANY (@add_user_id::uuid[]))
AND (@remove_user_id::uuid[] ISNULL
OR remove_user_id = ANY (@remove_user_id::uuid[]))
AND (
/* only include overrides that end after the search start */
sqlc.narg(search_start)::timestamptz ISNULL
OR o.end_time > @search_start)
AND (
/* only include overrides that start before/within the search end */
sqlc.narg(search_end)::timestamptz ISNULL
OR o.start_time <= @search_start)
AND (
/* resume search after specified "cursor" override */
@after_id::uuid ISNULL
OR (o.start_time > after.start_time
OR (o.start_time = after.start_time
AND o.end_time > after.end_time)
OR (o.start_time = after.start_time
AND o.end_time = after.end_time
AND o.id > after.id)))
ORDER BY
o.start_time,
o.end_time,
o.id
LIMIT 150;

170 changes: 58 additions & 112 deletions override/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ package override
import (
"context"
"database/sql"
"text/template"
"time"

"github.com/target/goalert/util/sqlutil"
"github.com/google/uuid"
"github.com/target/goalert/gadb"

"github.com/target/goalert/assignment"
"github.com/target/goalert/permission"
"github.com/target/goalert/search"
"github.com/target/goalert/validation/validate"

"github.com/pkg/errors"
)

// SearchOptions allow filtering and paginating the list of rotations.
Expand All @@ -39,135 +37,83 @@ type SearchCursor struct {
ID string `json:"i,omitempty"`
}

var searchTemplate = template.Must(template.New("search").Parse(`
{{if .After.ID}}
WITH after AS (
SELECT id, start_time, end_time
FROM user_overrides
WHERE id = :afterID
)
{{end}}
SELECT
o.id, o.start_time, o.end_time, add_user_id, remove_user_id, tgt_schedule_id
FROM user_overrides o
{{if .After.ID}}
JOIN after ON true
{{end}}
WHERE true
{{if .Omit}}
AND not o.id = any(:omit)
{{end}}
{{if .ScheduleID}}
AND tgt_schedule_id = :scheduleID
{{end}}
{{if .AnyUserIDs}}
AND (add_user_id = any(:anyUserIDs) OR remove_user_id = any(:anyUserIDs))
{{end}}
{{if .AddUserIDs}}
AND add_user_id = any(:addUserIDs)
{{end}}
{{if .RemoveUserIDs}}
AND remove_user_id = any(:removeUserIDs)
{{end}}
{{if not .Start.IsZero}}
AND o.end_time > :startTime
{{end}}
{{if not .End.IsZero}}
AND o.start_time <= :endTime
{{end}}
{{if .After.ID}}
AND (
o.start_time > after.start_time OR (
o.start_time = after.start_time AND
o.end_time > after.end_time
) OR (
o.start_time = after.start_time AND
o.end_time = after.end_time AND
o.id > after.id
)
)
{{end}}
ORDER BY o.start_time, o.end_time, o.id
LIMIT {{.Limit}}
`))

type renderData SearchOptions

func (opts renderData) Normalize() (*renderData, error) {
if opts.Limit == 0 {
opts.Limit = search.DefaultMaxResults
}

err := validate.Many(
validate.ManyUUID("AddUserIDs", opts.AddUserIDs, 10),
validate.ManyUUID("RemoveUserIDs", opts.RemoveUserIDs, 10),
validate.ManyUUID("AnyUserIDs", opts.RemoveUserIDs, 10),
validate.Range("Limit", opts.Limit, 0, search.MaxResults),
validate.ManyUUID("Omit", opts.Omit, 50),
)
if opts.ScheduleID != "" {
err = validate.Many(err, validate.UUID("ScheduleID", opts.ScheduleID))
func (s *Store) Search(ctx context.Context, db gadb.DBTX, opts *SearchOptions) ([]UserOverride, error) {
err := permission.LimitCheckAny(ctx, permission.User)
if err != nil {
return nil, err
}
if opts.After.ID != "" {
err = validate.Many(err, validate.UUID("After.ID", opts.After.ID))
if opts == nil {
opts = &SearchOptions{}
}

return &opts, err
}

func (opts renderData) QueryArgs() []sql.NamedArg {
return []sql.NamedArg{
sql.Named("afterID", opts.After.ID),
sql.Named("scheduleID", opts.ScheduleID),
sql.Named("startTime", opts.Start),
sql.Named("endTime", opts.End),
sql.Named("addUserIDs", sqlutil.UUIDArray(opts.AddUserIDs)),
sql.Named("removeUserIDs", sqlutil.UUIDArray(opts.RemoveUserIDs)),
sql.Named("anyUserIDs", sqlutil.UUIDArray(opts.AnyUserIDs)),
sql.Named("omit", sqlutil.UUIDArray(opts.Omit)),
if opts.Limit == 0 {
opts.Limit = search.DefaultMaxResults
}
}

func (s *Store) Search(ctx context.Context, opts *SearchOptions) ([]UserOverride, error) {
err := permission.LimitCheckAny(ctx, permission.User)
var arg gadb.OverrideSearchParams
arg.AddUserID, err = validate.ParseManyUUID("AddUserIDs", opts.AddUserIDs, 10)
if err != nil {
return nil, err
}
if opts == nil {
opts = &SearchOptions{}
arg.RemoveUserID, err = validate.ParseManyUUID("RemoveUserIDs", opts.RemoveUserIDs, 10)
if err != nil {
return nil, err
}
data, err := (*renderData)(opts).Normalize()
arg.AnyUserID, err = validate.ParseManyUUID("AnyUserIDs", opts.AnyUserIDs, 10)
if err != nil {
return nil, err
}
query, args, err := search.RenderQuery(ctx, searchTemplate, data)
arg.Omit, err = validate.ParseManyUUID("Omit", opts.Omit, 50)
if err != nil {
return nil, errors.Wrap(err, "render query")
return nil, err
}

rows, err := s.db.QueryContext(ctx, query, args...)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
if opts.ScheduleID != "" {
id, err := validate.ParseUUID("ScheduleID", opts.ScheduleID)
if err != nil {
return nil, err
}
arg.ScheduleID = uuid.NullUUID{UUID: id, Valid: true}
}
if !opts.Start.IsZero() {
arg.SearchStart = sql.NullTime{Time: opts.Start, Valid: true}
}
if !opts.End.IsZero() {
arg.SearchEnd = sql.NullTime{Time: opts.End, Valid: true}
}
if opts.After.ID != "" {
id, err := validate.ParseUUID("After.ID", opts.After.ID)
if err != nil {
return nil, err
}
arg.AfterID = uuid.NullUUID{UUID: id, Valid: true}
}

rows, err := gadb.New(db).OverrideSearch(ctx, arg)
if err != nil {
return nil, err
}
defer rows.Close()
if len(rows) > opts.Limit && opts.Limit > 0 {
rows = rows[:opts.Limit]
}

var result []UserOverride
var u UserOverride
var add, rem, schedID sql.NullString
for rows.Next() {
err = rows.Scan(&u.ID, &u.Start, &u.End, &add, &rem, &schedID)
if err != nil {
return nil, err
result := make([]UserOverride, len(rows))
for i, r := range rows {
var add, rem string
if r.AddUserID.Valid {
add = r.AddUserID.UUID.String()
}
u.AddUserID = add.String
u.RemoveUserID = rem.String
if schedID.Valid {
u.Target = assignment.ScheduleTarget(schedID.String)
if r.RemoveUserID.Valid {
rem = r.RemoveUserID.UUID.String()
}

result[i] = UserOverride{
ID: r.ID.String(),
Start: r.StartTime,
End: r.EndTime,
AddUserID: add,
RemoveUserID: rem,
Target: assignment.ScheduleTarget(r.TgtScheduleID.String()),
}
result = append(result, u)
}

return result, nil
Expand Down
Loading

0 comments on commit 3319556

Please sign in to comment.