package beads import "sourcecraft.dev/bigbes/sr-ht-dolt/browse" // --- the stored rows behind one issue ---------------------------------------- // // Everything else in this package is a reading: the Issue struct names the // columns bd surfaces and drops the rest, humanizeEvent turns two JSON blobs // into a sentence, an Edge keeps an id and a type out of a dependency row. Each // of those is a choice, and a choice can be wrong — a column bd added that no // field here models, a status this projection mapped to the wrong category, an // event whose stored old_value says something the humanized line does not — and // none of it is visible from the pane that made the choice. // // So the detail modes also carry the rows they were built from, as read. The // point is to be checkable against the rendering above it, which is why nothing // here is normalised, sorted or rewritten: the columns come in the order the // table was read in, the values are the strings browse returned, and a cell that // holds no value says so rather than rendering as one. // RawMax caps how many rows of one related table the raw section carries. The // issues row is one row by construction; a busy issue's events or comments are // not, and a section that dumps three hundred audit rows is not a check on the // rendering, it is a second page nobody reads. RawTable.Matched says how many // rows actually belong to the issue, so a capped table is visibly capped. const RawMax = 50 // RawCell is one stored cell: the column it came from, the value browse rendered // for it, and whether the cell holds a value at all. // // Null is the whole reason this type exists rather than a plain string pair. // browse renders a cell that holds no value as the text "NULL", which is exactly // what a cell storing those four characters renders as, and cell() resolves that // collision by flattening a real NULL to "". That is right everywhere else here // — a missing closed_at is rendered as nothing — and wrong in this one place: a // view whose purpose is to be compared against the database may not report an // absent cell and an empty string as the same thing. // // Value is "" for a null cell rather than the "NULL" browse printed. Carrying // that text would put the collision straight back: a consumer that renders Value // without reading Null would print "NULL" for both readings again, which is the // state this type exists to leave behind. type RawCell struct { Column string Value string Null bool } // RawRow is one stored row: its cells in the order the columns were read in. // Column order is data here — it is the table's own order, and a raw view that // sorted it would be hiding one of the things it is meant to show. type RawRow struct { Cells []RawCell } // RawTable is the rows one table contributed to one issue. // // Matched is how many rows of the table belong to the issue; Rows holds at most // RawMax of them, in table order. The two differ only when the cap bit, and that // difference is what the pane says out loud. type RawTable struct { Table string Rows []RawRow Matched int } // Clipped reports that the table had more rows for this issue than RawMax, so // Rows is the first of them and not all of them. func (t RawTable) Clipped() bool { return t.Matched > len(t.Rows) } // rawRow renders one row as stored, in the page's own column order. // // A cell's nullness is read exactly as cell() reads it — through rowCells.isNull, // which is the one place in this package that decides — so the raw section and // the fields above it can never disagree about which cells hold a value. func rawRow(columns []string, r rowCells) RawRow { out := RawRow{Cells: make([]RawCell, 0, len(columns))} for i, name := range columns { if i >= len(r.values) { // A row shorter than its header is a malformed page, not a null cell: // there is no cell here to report either way, so it is left out. break } if r.isNull(i) { out.Cells = append(out.Cells, RawCell{Column: name, Null: true}) continue } out.Cells = append(out.Cells, RawCell{Column: name, Value: r.values[i]}) } return out } // rawTableOf collects the rows of a page that belong to one issue, capped at // RawMax and counted whole. A table with no rows for this issue yields nil — the // pane lists the tables that had something to say, not every table it read. func rawTableOf(name string, page *browse.RowPage, match func(map[string]int, rowCells) bool) *RawTable { if page == nil { return nil } cols := indexCols(page.Columns) tbl := RawTable{Table: name} for _, r := range rowsOf(page) { if !match(cols, r) { continue } tbl.Matched++ if len(tbl.Rows) < RawMax { tbl.Rows = append(tbl.Rows, rawRow(page.Columns, r)) } } if tbl.Matched == 0 { return nil } return &tbl } // matchColumn matches the rows whose named column holds the wanted id — the // shape of every per-issue table here except dependencies, which has two such // columns and gets its own matcher at the call site. func matchColumn(column, want string) func(map[string]int, rowCells) bool { return func(cols map[string]int, r rowCells) bool { return cell(cols, r, column) == want } } // addRaw appends a table's rows to the raw section, skipping the tables that had // none. func (d *Data) addRaw(t *RawTable) { if t == nil { return } d.Raw = append(d.Raw, *t) }