| 1 |
|
package beads |
| 2 |
|
|
| 3 |
|
import "sourcecraft.dev/bigbes/sr-ht-dolt/browse" |
| 4 |
|
|
| 5 |
|
// --- the stored rows behind one issue ---------------------------------------- |
| 6 |
|
// |
| 7 |
|
// Everything else in this package is a reading: the Issue struct names the |
| 8 |
|
// columns bd surfaces and drops the rest, humanizeEvent turns two JSON blobs |
| 9 |
|
// into a sentence, an Edge keeps an id and a type out of a dependency row. Each |
| 10 |
|
// of those is a choice, and a choice can be wrong — a column bd added that no |
| 11 |
|
// field here models, a status this projection mapped to the wrong category, an |
| 12 |
|
// event whose stored old_value says something the humanized line does not — and |
| 13 |
|
// none of it is visible from the pane that made the choice. |
| 14 |
|
// |
| 15 |
|
// So the detail modes also carry the rows they were built from, as read. The |
| 16 |
|
// point is to be checkable against the rendering above it, which is why nothing |
| 17 |
|
// here is normalised, sorted or rewritten: the columns come in the order the |
| 18 |
|
// table was read in, the values are the strings browse returned, and a cell that |
| 19 |
|
// holds no value says so rather than rendering as one. |
| 20 |
|
|
| 21 |
|
// RawMax caps how many rows of one related table the raw section carries. The |
| 22 |
|
// issues row is one row by construction; a busy issue's events or comments are |
| 23 |
|
// not, and a section that dumps three hundred audit rows is not a check on the |
| 24 |
|
// rendering, it is a second page nobody reads. RawTable.Matched says how many |
| 25 |
|
// rows actually belong to the issue, so a capped table is visibly capped. |
| 26 |
|
const RawMax = 50 |
| 27 |
|
|
| 28 |
|
// RawCell is one stored cell: the column it came from, the value browse rendered |
| 29 |
|
// for it, and whether the cell holds a value at all. |
| 30 |
|
// |
| 31 |
|
// Null is the whole reason this type exists rather than a plain string pair. |
| 32 |
|
// browse renders a cell that holds no value as the text "NULL", which is exactly |
| 33 |
|
// what a cell storing those four characters renders as, and cell() resolves that |
| 34 |
|
// collision by flattening a real NULL to "". That is right everywhere else here |
| 35 |
|
// — a missing closed_at is rendered as nothing — and wrong in this one place: a |
| 36 |
|
// view whose purpose is to be compared against the database may not report an |
| 37 |
|
// absent cell and an empty string as the same thing. |
| 38 |
|
// |
| 39 |
|
// Value is "" for a null cell rather than the "NULL" browse printed. Carrying |
| 40 |
|
// that text would put the collision straight back: a consumer that renders Value |
| 41 |
|
// without reading Null would print "NULL" for both readings again, which is the |
| 42 |
|
// state this type exists to leave behind. |
| 43 |
|
type RawCell struct { |
| 44 |
|
Column string |
| 45 |
|
Value string |
| 46 |
|
Null bool |
| 47 |
|
} |
| 48 |
|
|
| 49 |
|
// RawRow is one stored row: its cells in the order the columns were read in. |
| 50 |
|
// Column order is data here — it is the table's own order, and a raw view that |
| 51 |
|
// sorted it would be hiding one of the things it is meant to show. |
| 52 |
|
type RawRow struct { |
| 53 |
|
Cells []RawCell |
| 54 |
|
} |
| 55 |
|
|
| 56 |
|
// RawTable is the rows one table contributed to one issue. |
| 57 |
|
// |
| 58 |
|
// Matched is how many rows of the table belong to the issue; Rows holds at most |
| 59 |
|
// RawMax of them, in table order. The two differ only when the cap bit, and that |
| 60 |
|
// difference is what the pane says out loud. |
| 61 |
|
type RawTable struct { |
| 62 |
|
Table string |
| 63 |
|
Rows []RawRow |
| 64 |
|
Matched int |
| 65 |
|
} |
| 66 |
|
|
| 67 |
|
// Clipped reports that the table had more rows for this issue than RawMax, so |
| 68 |
|
// Rows is the first of them and not all of them. |
| 69 |
2 |
func (t RawTable) Clipped() bool { return t.Matched > len(t.Rows) } |
| 70 |
|
|
| 71 |
|
// rawRow renders one row as stored, in the page's own column order. |
| 72 |
|
// |
| 73 |
|
// A cell's nullness is read exactly as cell() reads it — through rowCells.isNull, |
| 74 |
|
// which is the one place in this package that decides — so the raw section and |
| 75 |
|
// the fields above it can never disagree about which cells hold a value. |
| 76 |
181 |
func rawRow(columns []string, r rowCells) RawRow { |
| 77 |
181 |
out := RawRow{Cells: make([]RawCell, 0, len(columns))} |
| 78 |
879 |
for i, name := range columns { |
| 79 |
879 |
if i >= len(r.values) { |
| 80 |
1 |
// A row shorter than its header is a malformed page, not a null cell: |
| 81 |
1 |
// there is no cell here to report either way, so it is left out. |
| 82 |
1 |
break |
| 83 |
|
} |
| 84 |
878 |
if r.isNull(i) { |
| 85 |
49 |
out.Cells = append(out.Cells, RawCell{Column: name, Null: true}) |
| 86 |
49 |
continue |
| 87 |
|
} |
| 88 |
829 |
out.Cells = append(out.Cells, RawCell{Column: name, Value: r.values[i]}) |
| 89 |
|
} |
| 90 |
181 |
return out |
| 91 |
|
} |
| 92 |
|
|
| 93 |
|
// rawTableOf collects the rows of a page that belong to one issue, capped at |
| 94 |
|
// RawMax and counted whole. A table with no rows for this issue yields nil — the |
| 95 |
|
// pane lists the tables that had something to say, not every table it read. |
| 96 |
81 |
func rawTableOf(name string, page *browse.RowPage, match func(map[string]int, rowCells) bool) *RawTable { |
| 97 |
81 |
if page == nil { |
| 98 |
11 |
return nil |
| 99 |
11 |
} |
| 100 |
70 |
cols := indexCols(page.Columns) |
| 101 |
70 |
tbl := RawTable{Table: name} |
| 102 |
8223 |
for _, r := range rowsOf(page) { |
| 103 |
8223 |
if !match(cols, r) { |
| 104 |
6086 |
continue |
| 105 |
|
} |
| 106 |
2137 |
tbl.Matched++ |
| 107 |
2137 |
if len(tbl.Rows) < RawMax { |
| 108 |
180 |
tbl.Rows = append(tbl.Rows, rawRow(page.Columns, r)) |
| 109 |
180 |
} |
| 110 |
|
} |
| 111 |
70 |
if tbl.Matched == 0 { |
| 112 |
14 |
return nil |
| 113 |
14 |
} |
| 114 |
56 |
return &tbl |
| 115 |
|
} |
| 116 |
|
|
| 117 |
|
// matchColumn matches the rows whose named column holds the wanted id — the |
| 118 |
|
// shape of every per-issue table here except dependencies, which has two such |
| 119 |
|
// columns and gets its own matcher at the call site. |
| 120 |
61 |
func matchColumn(column, want string) func(map[string]int, rowCells) bool { |
| 121 |
8191 |
return func(cols map[string]int, r rowCells) bool { |
| 122 |
8191 |
return cell(cols, r, column) == want |
| 123 |
8191 |
} |
| 124 |
|
} |
| 125 |
|
|
| 126 |
|
// addRaw appends a table's rows to the raw section, skipping the tables that had |
| 127 |
|
// none. |
| 128 |
81 |
func (d *Data) addRaw(t *RawTable) { |
| 129 |
81 |
if t == nil { |
| 130 |
25 |
return |
| 131 |
25 |
} |
| 132 |
56 |
d.Raw = append(d.Raw, *t) |
| 133 |
|
} |