coverage~bigbes/sr-ht-dolt3523280cbeads/rows.go

Coverage
95.2% 80/84 statements
Δ
+0.0
Blob
8752021
1 package beads
2
3 import (
4 "context"
5 "errors"
6 "sort"
7 "strings"
8
9 "sourcecraft.dev/bigbes/sr-ht-dolt/browse"
10 )
11
12 // --- helpers -----------------------------------------------------------------
13
14 // readRows reads up to Max rows of a required table and its reported total.
15 351 func readRows(ctx context.Context, sess BrowseSession, ref, table string) (*browse.RowPage, int, error) {
16 351 page, err := sess.Rows(ctx, ref, table, 0, Max)
17 351 if err != nil {
18 0 return nil, 0, err
19 0 }
20 351 return page, page.Total, nil
21 }
22
23 // readRowsOptional is readRows for a table that may not exist: ErrTableNotFound
24 // degrades to (nil, 0, nil) so the caller can treat it as empty.
25 536 func readRowsOptional(ctx context.Context, sess BrowseSession, ref, table string) (*browse.RowPage, int, error) {
26 536 page, err := sess.Rows(ctx, ref, table, 0, Max)
27 536 if err != nil {
28 270 if errors.Is(err, browse.ErrTableNotFound) {
29 270 return nil, 0, nil
30 270 }
31 0 return nil, 0, err
32 }
33 266 return page, page.Total, nil
34 }
35
36 // indexCols builds a column-name → cell-index map from a RowPage's Columns, so
37 // cells are addressed by name regardless of the underlying column order.
38 763 func indexCols(cols []string) map[string]int {
39 763 m := make(map[string]int, len(cols))
40 3646 for i, c := range cols {
41 3646 m[c] = i
42 3646 }
43 763 return m
44 }
45
46 // rowCells is one row of a page together with the NULL mask browse returned
47 // beside it: the rendered strings, and the answer to "does this cell hold a
48 // value at all?" that the strings cannot carry.
49 //
50 // It is what every projection here iterates and what cell reads, so a row and
51 // its mask travel together and cannot be paired up wrongly at a call site.
52 type rowCells struct {
53 values []string
54
55 // nulls is browse's mask for this row, or nil for a page that carried none —
56 // which is a hand-built page, since browse fills one for every page it
57 // returns. See cell for what an absent mask means.
58 nulls []bool
59 }
60
61 // rowsOf pairs each row of a page with its own mask. A nil page — an optional
62 // table that is absent — has no rows, which is what the callers of
63 // readRowsOptional already treat it as.
64 //
65 // A row the mask does not cover gets a nil one rather than an all-false one: not
66 // knowing whether a cell holds a value is a different answer from knowing that
67 // it does, and cell reads the two differently.
68 1005 func rowsOf(page *browse.RowPage) []rowCells {
69 1005 if page == nil {
70 1 return nil
71 1 }
72 1004 out := make([]rowCells, 0, len(page.Rows))
73 99758 for i, r := range page.Rows {
74 99758 var mask []bool
75 99758 if i < len(page.Nulls) {
76 25 mask = page.Nulls[i]
77 25 }
78 99758 out = append(out, rowCells{values: r, nulls: mask})
79 }
80 1004 return out
81 }
82
83 // cell returns the named column's value for a row, or "" when the column is
84 // absent, out of range, or holds no value at all.
85 //
86 // A cell that holds no value reads as "": every projection renders a missing
87 // timestamp, assignee or close reason as nothing, and that must not change. What
88 // the mask changes is the other reading of the same string — browse renders a
89 // real NULL as the text "NULL", so a row that *stores* those four characters
90 // rendered identically and was flattened to "" too, which turned a stored title
91 // into an empty one. The mask beside the row answers which of the two it is, so
92 // it decides here rather than the string.
93 //
94 // A cell no mask covers is read the way this package read every cell before the
95 // mask existed: "NULL" is absent. That is not a guess dressed up as an answer —
96 // it is the older reading, kept for the only pages that lack a mask, which are
97 // the ones built by hand rather than read from a store. browse fills a mask
98 // parallel to the rows for every page it returns, so no read of a database
99 // arrives here without one.
100 271607 func cell(cols map[string]int, row rowCells, name string) string {
101 271607 i, ok := cols[name]
102 271607 if !ok || i < 0 || i >= len(row.values) {
103 20556 return ""
104 20556 }
105 251051 if row.isNull(i) {
106 530 return ""
107 530 }
108 250521 return row.values[i]
109 }
110
111 // isNull reports whether the i-th cell of this row holds no value.
112 //
113 // It is the one place in this package that decides, so cell — which flattens an
114 // absent cell to "" — and the raw section — which must not — can never come to
115 // different answers about the same cell. The reading is the one cell documents:
116 // the mask decides when the row has one, and a row with none falls back to the
117 // pre-mask reading, where the text "NULL" is how absence arrived.
118 //
119 // A cell past the end of the row is not a value, so it reads as absent.
120 251929 func (r rowCells) isNull(i int) bool {
121 251929 if i < 0 || i >= len(r.values) {
122 0 return true
123 0 }
124 251929 if i < len(r.nulls) {
125 103 return r.nulls[i]
126 103 }
127 251826 return r.values[i] == "NULL"
128 }
129
130 // indexStatusCategories maps a status name (lowercased) to its category, from
131 // the optional custom_statuses table. A nil page — the table is absent — yields
132 // an empty map, and statusCategory then falls back to its name heuristics.
133 178 func indexStatusCategories(statuses *browse.RowPage) map[string]string {
134 178 out := map[string]string{}
135 178 if statuses == nil {
136 106 return out
137 106 }
138 72 cols := indexCols(statuses.Columns)
139 6205 for _, r := range rowsOf(statuses) {
140 6205 if name := cell(cols, r, "name"); name != "" {
141 6205 out[strings.ToLower(name)] = strings.ToLower(cell(cols, r, "category"))
142 6205 }
143 }
144 72 return out
145 }
146
147 // indexLabels maps issue id → its label names, from the optional labels table.
148 // A nil page yields an empty map.
149 178 func indexLabels(labels *browse.RowPage) map[string][]string {
150 178 out := map[string][]string{}
151 178 if labels == nil {
152 142 return out
153 142 }
154 36 cols := indexCols(labels.Columns)
155 10076 for _, r := range rowsOf(labels) {
156 10076 id := cell(cols, r, "issue_id")
157 10076 lb := cell(cols, r, "label")
158 10076 if id != "" && lb != "" {
159 10076 out[id] = append(out[id], lb)
160 10076 }
161 }
162 36 return out
163 }
164
165 // indexIssueCategories maps issue id → status category (open / in_progress /
166 // closed), which is what decides whether a blocking target still blocks.
167 173 func indexIssueCategories(issues *browse.RowPage, cols map[string]int, catByStatus map[string]string) map[string]string {
168 173 out := make(map[string]string, len(issues.Rows))
169 20759 for _, r := range rowsOf(issues) {
170 20759 out[cell(cols, r, "id")] = statusCategory(cell(cols, r, "status"), catByStatus)
171 20759 }
172 173 return out
173 }
174
175 // depIndex is the dependency edge set aggregated per issue: how many things an
176 // issue waits on, how many wait on it, and whether any of the things it waits on
177 // is still open (which is what "blocked" means).
178 type depIndex struct {
179 blockedByCount map[string]int // issue_id → #deps it has
180 blocksCount map[string]int // depends_on_issue_id → #deps aimed at it
181 blockedOpen map[string]bool // issue_id → has an open blocking dep
182 }
183
184 // indexDeps aggregates the dependencies table. A "blocks" edge to a still-open
185 // target blocks its source; parent-child is hierarchy, not a blocker — a subtask
186 // is not blocked by its (open) epic, matching bd's own is_blocked/ready
187 // accounting. A nil page (the table is absent) yields empty maps.
188 173 func indexDeps(deps *browse.RowPage, catByIssue map[string]string) depIndex {
189 173 idx := depIndex{
190 173 blockedByCount: map[string]int{},
191 173 blocksCount: map[string]int{},
192 173 blockedOpen: map[string]bool{},
193 173 }
194 173 if deps == nil {
195 0 return idx
196 0 }
197 173 cols := indexCols(deps.Columns)
198 6217 for _, r := range rowsOf(deps) {
199 6217 from := cell(cols, r, "issue_id")
200 6217 to := cell(cols, r, "depends_on_issue_id")
201 6217 typ := strings.ToLower(cell(cols, r, "type"))
202 6217 if from != "" {
203 6217 idx.blockedByCount[from]++
204 6217 }
205 6217 if to != "" {
206 6217 idx.blocksCount[to]++
207 6217 }
208 6217 if from != "" && typ == "blocks" && catByIssue[to] != "closed" {
209 6200 idx.blockedOpen[from] = true
210 6200 }
211 }
212 173 return idx
213 }
214
215 // readyRow is bd's ready rule, and the only copy of it: an issue is ready when
216 // it is open (not in-progress, not closed), unblocked, and not a
217 // template/ephemeral scaffold. Derived in-process from the issues rows already
218 // loaded rather than by reading the ready_issues table.
219 //
220 // Every surface that says "ready" — the board's ⚡ marker and its ?ready=1
221 // filter, the cross-database /ready page, the MCP ready_work tool — comes
222 // through here, because two surfaces that each spelled the rule out would
223 // disagree the first time it changed.
224 14533 func readyRow(cat string, blocked bool, row rowCells, cols map[string]int) bool {
225 14533 return cat == "open" && !blocked &&
226 14533 !truthy(cell(cols, row, "is_template")) && !truthy(cell(cols, row, "ephemeral"))
227 14533 }
228
229 // truthy reports whether a cell reads as a set boolean/flag.
230 23076 func truthy(s string) bool {
231 23076 switch strings.ToLower(strings.TrimSpace(s)) {
232 68 case "1", "true", "yes", "t", "y":
233 68 return true
234 }
235 23008 return false
236 }
237
238 // sortedKeys returns a set's keys in ascending order.
239 262 func sortedKeys(set map[string]bool) []string {
240 262 out := make([]string, 0, len(set))
241 6628 for k := range set {
242 6628 out = append(out, k)
243 6628 }
244 262 sort.Strings(out)
245 262 return out
246 }
247
248 // containsString reports whether s is in xs.
249 4 func containsString(xs []string, s string) bool {
250 4 for _, x := range xs {
251 2 if x == s {
252 1 return true
253 1 }
254 }
255 3 return false
256 }