package graph
import (
"context"
"strconv"
coremodel "sourcecraft.dev/bigbes/sr-ht-core/model"
"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
"sourcecraft.dev/bigbes/sr-ht-dolt/graph/model"
)
// Page sizes for the database listings. The count travels in the cursor, as it
// does on every other sr.ht service, so a client that asked for a page size
// keeps it across pages without repeating itself.
const (
defaultPageSize = 25
maxPageSize = 100
)
// page slices one page out of a listing the store has already filtered to what
// the caller may see, and mints the cursor for the next one.
//
// The slicing is in memory rather than in SQL, which is a real limit and not an
// oversight: ListReposForViewer's rule spans three tables and the listings it
// answers are instance-sized (tens of rows), so the whole set is fetched and cut
// here. A listing that outgrows that wants a keyset query in db/, and this is
// the function that would then become a thin wrapper over it.
//
// The cursor carries the id of the first row of the next page and the page size.
// Resuming starts at the first row whose id is at or below it, so a cursor whose
// row was deleted — or made invisible to this caller — between two pages resumes
// at the next row instead of failing or, worse, silently starting over.
func page(repos []*core.Repo, cursor *coremodel.Cursor, filter *coremodel.Filter) *model.DatabaseCursor {
count := defaultPageSize
start := 0
if cursor != nil {
if cursor.Count > 0 {
count = cursor.Count
}
if after, err := strconv.Atoi(cursor.Next); err == nil {
start = len(repos)
for i, repo := range repos {
if repo.ID <= after {
start = i
break
}
}
}
}
// A filter given on this call wins over what the cursor remembers, so a
// client can change the page size mid-walk without minting a new cursor.
if filter != nil && filter.Count != nil && *filter.Count > 0 {
count = *filter.Count
}
if count > maxPageSize {
count = maxPageSize
}
end := start + count
if end > len(repos) {
end = len(repos)
}
out := make([]*model.Database, 0, end-start)
for _, repo := range repos[start:end] {
out = append(out, model.NewDatabase(repo))
}
page := &model.DatabaseCursor{Results: out}
if end < len(repos) {
page.Cursor = &coremodel.Cursor{
Next: strconv.Itoa(repos[end].ID),
Count: count,
}
}
return page
}
// logLimit bounds how many commits one Database.log answers. The cap is the
// surface's, not the client's: a log page is a walk of the on-disk store.
func logLimit(limit *int) int {
if limit == nil || *limit <= 0 {
return defaultLogLimit
}
if *limit > maxLogLimit {
return maxLogLimit
}
return *limit
}
// cursorNext is the commit hash a log page resumes from, or "" to start at the
// ref's head.
func cursorNext(from *coremodel.Cursor) string {
if from == nil {
return ""
}
return from.Next
}
// nextCursor wraps browse's "next hash" in the schema's cursor, or nil when the
// walk reached the root commit.
func nextCursor(next string) *coremodel.Cursor {
if next == "" {
return nil
}
return &coremodel.Cursor{Next: next}
}
// resolveRef answers the ref a field should read: the one the query named, or
// the database's default branch. It returns "" — and no error — for a store
// with no branches at all, which is a database nothing has been pushed to yet;
// the caller turns that into the empty answer for its own field.
func resolveRef(ctx context.Context, sess BrowseSession, ref *string) (string, error) {
if ref != nil && *ref != "" {
return *ref, nil
}
branches, err := sess.Branches(ctx)
if err != nil {
return "", internalError("Database.ref", err)
}
if len(branches) == 0 {
return "", nil
}
return browse.DefaultBranch(branches), nil
}