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)
}