coverage~bigbes/sr-ht-doltede3b0bbbrowse/tables.go

Coverage
0.0% 0/148 statements
Δ
Blob
953c9b3
1 package browse
2
3 import (
4 "context"
5 "fmt"
6 "io"
7
8 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
9 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb/durable"
10 "github.com/dolthub/dolt/go/libraries/doltcore/schema"
11 "github.com/dolthub/dolt/go/store/prolly/tree"
12 "github.com/dolthub/dolt/go/store/val"
13 )
14
15 // Cell placeholders. Rendering must never panic and never emit non-printable
16 // bytes; exotic or out-of-band values degrade to one of these.
17 const (
18 placeholderNull = "NULL"
19 placeholderBinary = "<binary>"
20 placeholderUnreadable = "<unreadable>"
21 )
22
23 // ColumnInfo describes one column of a table schema.
24 type ColumnInfo struct {
25 Name string
26 Type string
27 PrimaryKey bool
28 Nullable bool
29 }
30
31 // TableInfo is a table name, its schema, and its row count at a ref.
32 type TableInfo struct {
33 Name string
34 Columns []ColumnInfo
35 RowCount uint64
36 }
37
38 // RowPage is a paginated slice of a table's rows rendered to strings. Columns
39 // lists the column names in the same order as each row's cells. For keyed
40 // tables columns are primary-key columns first, then the rest.
41 //
42 // # Why a NULL renders as "NULL" and there is a mask beside it
43 //
44 // A cell that holds no value renders as the string "NULL" in Rows, which is
45 // exactly what a row that genuinely stores the four characters N,U,L,L renders
46 // as. That flattening stays: Rows is what pages and projections put on screen,
47 // where "NULL" is the conventional and readable answer, and every consumer of
48 // this package reads Rows as display strings — encoding nullness into the
49 // string instead (a sentinel, an empty string, a marker) would ripple through
50 // every template and every projection for no gain to a reader.
51 //
52 // Nulls is for the callers where the two are not interchangeable: an API that
53 // hands rows to a machine (the MCP row reader) gives out strings with no schema
54 // beside them, so without the mask an agent cannot tell an absent value from
55 // the text "NULL" — and answering "is there a value here?" is not something it
56 // can recover from the rendered string afterwards.
57 //
58 // Nulls is parallel to Rows: Nulls[i][j] reports whether Rows[i][j] was a real
59 // SQL NULL, so any index valid for Rows is valid for Nulls. It costs one bool
60 // per cell, which is what makes it affordable on a full page.
61 //
62 // Only NULL gets this treatment. The other two placeholders, "<binary>" and
63 // "<unreadable>", answer what a value *is*; NULL answers whether there is one
64 // at all, and only that question is unanswerable from the rendered string.
65 type RowPage struct {
66 Columns []string
67 Rows [][]string
68 // Nulls[i][j] is true when Rows[i][j] is a real NULL rather than a value
69 // that happens to render like one. Same shape as Rows.
70 Nulls [][]bool
71 Offset int
72 Total int
73 }
74
75 // Tables lists the tables in the committed root at ref (a branch name or
76 // commit hash), each with its schema and row count.
77 0 func (db *DB) Tables(ctx context.Context, refStr string) ([]TableInfo, error) {
78 0 root, err := db.resolveRoot(ctx, refStr)
79 0 if err != nil {
80 0 return nil, err
81 0 }
82
83 0 names, err := root.GetTableNames(ctx, doltdb.DefaultSchemaName, false)
84 0 if err != nil {
85 0 return nil, fmt.Errorf("browse: list tables at %q: %w", refStr, err)
86 0 }
87
88 0 infos := make([]TableInfo, 0, len(names))
89 0 for _, name := range names {
90 0 tbl, ok, err := root.GetTable(ctx, doltdb.TableName{Name: name})
91 0 if err != nil {
92 0 return nil, fmt.Errorf("browse: get table %q: %w", name, err)
93 0 }
94 0 if !ok {
95 0 // Listed by GetTableNames but not resolvable: inconsistent root.
96 0 return nil, fmt.Errorf("browse: table %q listed but missing", name)
97 0 }
98
99 0 sch, err := tbl.GetSchema(ctx)
100 0 if err != nil {
101 0 return nil, fmt.Errorf("browse: schema of %q: %w", name, err)
102 0 }
103
104 0 idx, err := tbl.GetRowData(ctx)
105 0 if err != nil {
106 0 return nil, fmt.Errorf("browse: row data of %q: %w", name, err)
107 0 }
108 0 count, err := idx.Count()
109 0 if err != nil {
110 0 return nil, fmt.Errorf("browse: row count of %q: %w", name, err)
111 0 }
112
113 0 infos = append(infos, TableInfo{
114 0 Name: name,
115 0 Columns: columnInfos(sch),
116 0 RowCount: count,
117 0 })
118 }
119
120 0 return infos, nil
121 }
122
123 // TableHash returns the content hash of one table in the committed root at ref
124 // (a branch name or a commit hash). It reads no rows: the hash is the address
125 // of the table struct itself, so answering "did this table change between two
126 // commits?" costs a root lookup and nothing more. That is what makes a history
127 // walk affordable — a walk that has to attribute a change to a commit can skip
128 // every commit whose table hash equals its neighbour's, and read rows only at
129 // the few commits that actually touched the table.
130 //
131 // The hash covers the whole table (schema, row data, secondary indexes), not
132 // just the rows. For change detection that is the wanted answer: a column added
133 // without touching a row is still a change to the table.
134 //
135 // A table that does not exist at ref is ok=false with a nil error, deliberately
136 // *not* ErrTableNotFound (which Rows returns). The caller of this primitive is
137 // a loop walking backwards through history asking "did it change?", and a table
138 // that had not been created yet at an old commit is an ordinary answer there,
139 // not a failure. Callers that need "absent" to be an error can test ok
140 // themselves; a caller that walked into an error at every pre-creation commit
141 // could not tell that case apart from a real one.
142 0 func (db *DB) TableHash(ctx context.Context, refStr, table string) (string, bool, error) {
143 0 root, err := db.resolveRoot(ctx, refStr)
144 0 if err != nil {
145 0 return "", false, err
146 0 }
147
148 0 tbl, ok, err := root.GetTable(ctx, doltdb.TableName{Name: table})
149 0 if err != nil {
150 0 return "", false, fmt.Errorf("browse: get table %q at %q: %w", table, refStr, err)
151 0 }
152 0 if !ok {
153 0 return "", false, nil
154 0 }
155
156 0 h, err := tbl.HashOf()
157 0 if err != nil {
158 0 return "", false, fmt.Errorf("browse: hash of table %q at %q: %w", table, refStr, err)
159 0 }
160 0 return h.String(), true, nil
161 }
162
163 // columnInfos renders a schema's columns in natural table order.
164 0 func columnInfos(sch schema.Schema) []ColumnInfo {
165 0 cols := sch.GetAllCols().GetColumns()
166 0 out := make([]ColumnInfo, len(cols))
167 0 for i, c := range cols {
168 0 typeStr := ""
169 0 if c.TypeInfo != nil {
170 0 if sqlType := c.TypeInfo.ToSqlType(); sqlType != nil {
171 0 typeStr = sqlType.String()
172 0 } else {
173 0 typeStr = c.TypeInfo.String()
174 0 }
175 }
176 0 out[i] = ColumnInfo{
177 0 Name: c.Name,
178 0 Type: typeStr,
179 0 PrimaryKey: c.IsPartOfPK,
180 0 Nullable: c.IsNullable(),
181 0 }
182 }
183 0 return out
184 }
185
186 // cellRef locates a column's value within a prolly row: either in the key
187 // tuple or the value tuple, at the given field index.
188 type cellRef struct {
189 fromKey bool
190 idx int
191 }
192
193 // Rows returns a page of rows from table at ref, starting at offset (0-based)
194 // and returning at most limit rows. The page is read directly from the prolly
195 // map via an ordinal range, so it is O(limit) regardless of offset.
196 0 func (db *DB) Rows(ctx context.Context, refStr, table string, offset, limit int) (*RowPage, error) {
197 0 if offset < 0 {
198 0 return nil, fmt.Errorf("browse: offset must be non-negative, got %d", offset)
199 0 }
200 0 if limit <= 0 {
201 0 return nil, fmt.Errorf("browse: limit must be positive, got %d", limit)
202 0 }
203
204 0 root, err := db.resolveRoot(ctx, refStr)
205 0 if err != nil {
206 0 return nil, err
207 0 }
208
209 0 tbl, ok, err := root.GetTable(ctx, doltdb.TableName{Name: table})
210 0 if err != nil {
211 0 return nil, fmt.Errorf("browse: get table %q: %w", table, err)
212 0 }
213 0 if !ok {
214 0 return nil, fmt.Errorf("%w: %s", ErrTableNotFound, table)
215 0 }
216
217 0 sch, err := tbl.GetSchema(ctx)
218 0 if err != nil {
219 0 return nil, fmt.Errorf("browse: schema of %q: %w", table, err)
220 0 }
221
222 0 idx, err := tbl.GetRowData(ctx)
223 0 if err != nil {
224 0 return nil, fmt.Errorf("browse: row data of %q: %w", table, err)
225 0 }
226 0 total, err := idx.Count()
227 0 if err != nil {
228 0 return nil, fmt.Errorf("browse: row count of %q: %w", table, err)
229 0 }
230
231 0 colNames, refs := rowLayout(sch)
232 0
233 0 page := &RowPage{
234 0 Columns: colNames,
235 0 Rows: [][]string{},
236 0 Nulls: [][]bool{},
237 0 Offset: offset,
238 0 Total: int(total),
239 0 }
240 0
241 0 start := uint64(offset)
242 0 if start >= total {
243 0 // Past the end (also covers the empty-table case): no rows.
244 0 return page, nil
245 0 }
246 0 stop := start + uint64(limit)
247 0 if stop > total {
248 0 stop = total
249 0 }
250
251 0 m, err := durable.ProllyMapFromIndex(idx)
252 0 if err != nil {
253 0 return nil, fmt.Errorf("browse: prolly map of %q: %w", table, err)
254 0 }
255 0 keyDesc, valDesc := m.Descriptors()
256 0
257 0 iter, err := m.IterOrdinalRange(ctx, start, stop)
258 0 if err != nil {
259 0 return nil, fmt.Errorf("browse: iterate rows of %q: %w", table, err)
260 0 }
261
262 // ns dereferences out-of-line (address-encoded) values — text/longtext that
263 // Dolt stores in a separate chunk rather than inline in the tuple.
264 0 ns := m.NodeStore()
265 0 for {
266 0 key, value, err := iter.Next(ctx)
267 0 if err == io.EOF {
268 0 break
269 }
270 0 if err != nil {
271 0 return nil, fmt.Errorf("browse: read row of %q: %w", table, err)
272 0 }
273
274 0 row := make([]string, len(refs))
275 0 // One bool per cell, appended in lockstep with the row so the two can
276 0 // never drift apart — including on a page that starts mid-table.
277 0 nulls := make([]bool, len(refs))
278 0 for i, r := range refs {
279 0 if r.fromKey {
280 0 row[i], nulls[i] = renderCell(ctx, ns, keyDesc, r.idx, key)
281 0 } else {
282 0 row[i], nulls[i] = renderCell(ctx, ns, valDesc, r.idx, value)
283 0 }
284 }
285 0 page.Rows = append(page.Rows, row)
286 0 page.Nulls = append(page.Nulls, nulls)
287 }
288
289 0 return page, nil
290 }
291
292 // rowLayout maps schema columns to their position in the prolly key/value
293 // tuples and produces the display column order.
294 //
295 // - Keyed tables: primary-key columns (in key order) map to the key tuple,
296 // the remaining columns (in stored order) to the value tuple.
297 // - Keyless tables: every column is in the value tuple; field 0 is the
298 // hidden cardinality, so column i lives at value index i+1, and there is
299 // no meaningful key.
300 0 func rowLayout(sch schema.Schema) ([]string, []cellRef) {
301 0 if schema.IsKeyless(sch) {
302 0 cols := sch.GetNonPKCols().GetColumns()
303 0 names := make([]string, len(cols))
304 0 refs := make([]cellRef, len(cols))
305 0 for i, c := range cols {
306 0 names[i] = c.Name
307 0 refs[i] = cellRef{fromKey: false, idx: i + 1}
308 0 }
309 0 return names, refs
310 }
311
312 0 pkCols := sch.GetPKCols().GetColumns()
313 0 nonPKCols := sch.GetNonPKCols().GetColumns()
314 0 names := make([]string, 0, len(pkCols)+len(nonPKCols))
315 0 refs := make([]cellRef, 0, len(pkCols)+len(nonPKCols))
316 0 for i, c := range pkCols {
317 0 names = append(names, c.Name)
318 0 refs = append(refs, cellRef{fromKey: true, idx: i})
319 0 }
320 0 for i, c := range nonPKCols {
321 0 names = append(names, c.Name)
322 0 refs = append(refs, cellRef{fromKey: false, idx: i})
323 0 }
324 0 return names, refs
325 }
326
327 // renderCell renders one tuple field to a display string. It never panics
328 // (recovering into a placeholder) and maps binary / out-of-band encodings to
329 // "<binary>" so a row preview never dumps opaque bytes.
330 //
331 // text/longtext columns are the exception: Dolt stores anything past a small
332 // inline threshold out-of-line, addressed by a content hash (StringAddrEnc, or
333 // StringAdaptiveEnc which is inline-or-address in the same field). Those are
334 // resolved through ns to their real content — without this they would render as
335 // "<binary>" (addr) or a raw hash string (adaptive), losing every long
336 // description, close reason, comment body, and audit payload.
337 //
338 // isNull is true only when the field holds no value. It is the answer the
339 // rendered string cannot carry, since a stored "NULL" renders the same way; see
340 // the RowPage doc. It comes from the nullness test the renderer already had to
341 // do, so reporting it costs no extra read of the tuple. A field that is
342 // unreadable (out of range, or a panic recovered here) is not null: nothing is
343 // known about it, which is a different answer from "there is no value".
344 0 func renderCell(ctx context.Context, ns tree.NodeStore, td *val.TupleDesc, i int, tup val.Tuple) (out string, isNull bool) {
345 0 defer func() {
346 0 if r := recover(); r != nil {
347 0 out, isNull = placeholderUnreadable, false
348 0 }
349 }()
350
351 0 if i < 0 || i >= td.Count() {
352 0 // Column not present in this tuple (e.g. a virtual/dropped column that
353 0 // isn't materialized): degrade rather than index out of range.
354 0 return placeholderUnreadable, false
355 0 }
356 0 if td.IsNull(i, tup) {
357 0 return placeholderNull, true
358 0 }
359
360 0 switch td.Types[i].Enc {
361 0 case val.StringAddrEnc:
362 0 return resolveStringAddr(ctx, ns, td, i, tup)
363 0 case val.StringAdaptiveEnc:
364 0 return resolveStringAdaptive(ctx, ns, td, i, tup)
365 case val.ByteStringEnc, val.Hash128Enc, val.CellEnc,
366 val.BytesAddrEnc, val.JSONAddrEnc,
367 val.GeomAddrEnc, val.CommitAddrEnc,
368 0 val.BytesAdaptiveEnc, val.GeomAdaptiveEnc:
369 0 // Genuine binary / opaque out-of-band values: never dump raw bytes.
370 0 return placeholderBinary, false
371 }
372
373 0 return td.FormatValue(ctx, i, td.GetField(i, tup)), false
374 }
375
376 // resolveStringAddr dereferences a StringAddrEnc field (text/longtext always
377 // stored out-of-line) to its full content. It returns the same (string,
378 // isNull) pair as renderCell.
379 0 func resolveStringAddr(ctx context.Context, ns tree.NodeStore, td *val.TupleDesc, i int, tup val.Tuple) (string, bool) {
380 0 h, ok := td.GetStringAddr(i, tup)
381 0 if !ok {
382 0 return placeholderNull, true
383 0 }
384 0 s, err := val.NewTextStorage(h, ns).Unwrap(ctx)
385 0 if err != nil {
386 0 return placeholderUnreadable, false
387 0 }
388 0 return s, false
389 }
390
391 // resolveStringAdaptive reads a StringAdaptiveEnc field, which stores its value
392 // either inline (returned as a string) or out-of-line (returned as a
393 // *val.TextStorage to Unwrap) in the same field. It returns the same (string,
394 // isNull) pair as renderCell.
395 0 func resolveStringAdaptive(ctx context.Context, ns tree.NodeStore, td *val.TupleDesc, i int, tup val.Tuple) (string, bool) {
396 0 v, ok, err := td.GetStringAdaptiveValue(ctx, i, ns, tup)
397 0 if err != nil || !ok {
398 0 if err != nil {
399 0 return placeholderUnreadable, false
400 0 }
401 0 return placeholderNull, true
402 }
403 0 switch s := v.(type) {
404 0 case string:
405 0 return s, false
406 0 case *val.TextStorage:
407 0 str, err := s.Unwrap(ctx)
408 0 if err != nil {
409 0 return placeholderUnreadable, false
410 0 }
411 0 return str, false
412 0 default:
413 0 return placeholderUnreadable, false
414 }
415 }