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 }