~bigbes/sr-ht-dolt

ref: 8ed47c67fd2f11b540cb8caa4c84407834b973af sr-ht-dolt/browse/tables.go -rw-r--r-- 7.5 KiB
8ed47c67 — Eugene Blikh build: pure-Go (CGO_ENABLED=0) build via a klauspost-backed gozstd shim 30 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
package browse

import (
	"context"
	"fmt"
	"io"

	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb/durable"
	"github.com/dolthub/dolt/go/libraries/doltcore/schema"
	"github.com/dolthub/dolt/go/store/val"
)

// Cell placeholders. Rendering must never panic and never emit non-printable
// bytes; exotic or out-of-band values degrade to one of these.
const (
	placeholderNull       = "NULL"
	placeholderBinary     = "<binary>"
	placeholderUnreadable = "<unreadable>"
)

// ColumnInfo describes one column of a table schema.
type ColumnInfo struct {
	Name       string
	Type       string
	PrimaryKey bool
	Nullable   bool
}

// TableInfo is a table name, its schema, and its row count at a ref.
type TableInfo struct {
	Name     string
	Columns  []ColumnInfo
	RowCount uint64
}

// RowPage is a paginated slice of a table's rows rendered to strings. Columns
// lists the column names in the same order as each row's cells. For keyed
// tables columns are primary-key columns first, then the rest.
type RowPage struct {
	Columns []string
	Rows    [][]string
	Offset  int
	Total   int
}

// Tables lists the tables in the committed root at ref (a branch name or
// commit hash), each with its schema and row count.
func (db *DB) Tables(ctx context.Context, refStr string) ([]TableInfo, error) {
	root, err := db.resolveRoot(ctx, refStr)
	if err != nil {
		return nil, err
	}

	names, err := root.GetTableNames(ctx, doltdb.DefaultSchemaName, false)
	if err != nil {
		return nil, fmt.Errorf("browse: list tables at %q: %w", refStr, err)
	}

	infos := make([]TableInfo, 0, len(names))
	for _, name := range names {
		tbl, ok, err := root.GetTable(ctx, doltdb.TableName{Name: name})
		if err != nil {
			return nil, fmt.Errorf("browse: get table %q: %w", name, err)
		}
		if !ok {
			// Listed by GetTableNames but not resolvable: inconsistent root.
			return nil, fmt.Errorf("browse: table %q listed but missing", name)
		}

		sch, err := tbl.GetSchema(ctx)
		if err != nil {
			return nil, fmt.Errorf("browse: schema of %q: %w", name, err)
		}

		idx, err := tbl.GetRowData(ctx)
		if err != nil {
			return nil, fmt.Errorf("browse: row data of %q: %w", name, err)
		}
		count, err := idx.Count()
		if err != nil {
			return nil, fmt.Errorf("browse: row count of %q: %w", name, err)
		}

		infos = append(infos, TableInfo{
			Name:     name,
			Columns:  columnInfos(sch),
			RowCount: count,
		})
	}

	return infos, nil
}

// columnInfos renders a schema's columns in natural table order.
func columnInfos(sch schema.Schema) []ColumnInfo {
	cols := sch.GetAllCols().GetColumns()
	out := make([]ColumnInfo, len(cols))
	for i, c := range cols {
		typeStr := ""
		if c.TypeInfo != nil {
			if sqlType := c.TypeInfo.ToSqlType(); sqlType != nil {
				typeStr = sqlType.String()
			} else {
				typeStr = c.TypeInfo.String()
			}
		}
		out[i] = ColumnInfo{
			Name:       c.Name,
			Type:       typeStr,
			PrimaryKey: c.IsPartOfPK,
			Nullable:   c.IsNullable(),
		}
	}
	return out
}

// cellRef locates a column's value within a prolly row: either in the key
// tuple or the value tuple, at the given field index.
type cellRef struct {
	fromKey bool
	idx     int
}

// Rows returns a page of rows from table at ref, starting at offset (0-based)
// and returning at most limit rows. The page is read directly from the prolly
// map via an ordinal range, so it is O(limit) regardless of offset.
func (db *DB) Rows(ctx context.Context, refStr, table string, offset, limit int) (*RowPage, error) {
	if offset < 0 {
		return nil, fmt.Errorf("browse: offset must be non-negative, got %d", offset)
	}
	if limit <= 0 {
		return nil, fmt.Errorf("browse: limit must be positive, got %d", limit)
	}

	root, err := db.resolveRoot(ctx, refStr)
	if err != nil {
		return nil, err
	}

	tbl, ok, err := root.GetTable(ctx, doltdb.TableName{Name: table})
	if err != nil {
		return nil, fmt.Errorf("browse: get table %q: %w", table, err)
	}
	if !ok {
		return nil, fmt.Errorf("%w: %s", ErrTableNotFound, table)
	}

	sch, err := tbl.GetSchema(ctx)
	if err != nil {
		return nil, fmt.Errorf("browse: schema of %q: %w", table, err)
	}

	idx, err := tbl.GetRowData(ctx)
	if err != nil {
		return nil, fmt.Errorf("browse: row data of %q: %w", table, err)
	}
	total, err := idx.Count()
	if err != nil {
		return nil, fmt.Errorf("browse: row count of %q: %w", table, err)
	}

	colNames, refs := rowLayout(sch)

	page := &RowPage{
		Columns: colNames,
		Rows:    [][]string{},
		Offset:  offset,
		Total:   int(total),
	}

	start := uint64(offset)
	if start >= total {
		// Past the end (also covers the empty-table case): no rows.
		return page, nil
	}
	stop := start + uint64(limit)
	if stop > total {
		stop = total
	}

	m, err := durable.ProllyMapFromIndex(idx)
	if err != nil {
		return nil, fmt.Errorf("browse: prolly map of %q: %w", table, err)
	}
	keyDesc, valDesc := m.Descriptors()

	iter, err := m.IterOrdinalRange(ctx, start, stop)
	if err != nil {
		return nil, fmt.Errorf("browse: iterate rows of %q: %w", table, err)
	}

	for {
		key, value, err := iter.Next(ctx)
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("browse: read row of %q: %w", table, err)
		}

		row := make([]string, len(refs))
		for i, r := range refs {
			if r.fromKey {
				row[i] = renderCell(ctx, keyDesc, r.idx, key)
			} else {
				row[i] = renderCell(ctx, valDesc, r.idx, value)
			}
		}
		page.Rows = append(page.Rows, row)
	}

	return page, nil
}

// rowLayout maps schema columns to their position in the prolly key/value
// tuples and produces the display column order.
//
//   - Keyed tables: primary-key columns (in key order) map to the key tuple,
//     the remaining columns (in stored order) to the value tuple.
//   - Keyless tables: every column is in the value tuple; field 0 is the
//     hidden cardinality, so column i lives at value index i+1, and there is
//     no meaningful key.
func rowLayout(sch schema.Schema) ([]string, []cellRef) {
	if schema.IsKeyless(sch) {
		cols := sch.GetNonPKCols().GetColumns()
		names := make([]string, len(cols))
		refs := make([]cellRef, len(cols))
		for i, c := range cols {
			names[i] = c.Name
			refs[i] = cellRef{fromKey: false, idx: i + 1}
		}
		return names, refs
	}

	pkCols := sch.GetPKCols().GetColumns()
	nonPKCols := sch.GetNonPKCols().GetColumns()
	names := make([]string, 0, len(pkCols)+len(nonPKCols))
	refs := make([]cellRef, 0, len(pkCols)+len(nonPKCols))
	for i, c := range pkCols {
		names = append(names, c.Name)
		refs = append(refs, cellRef{fromKey: true, idx: i})
	}
	for i, c := range nonPKCols {
		names = append(names, c.Name)
		refs = append(refs, cellRef{fromKey: false, idx: i})
	}
	return names, refs
}

// renderCell renders one tuple field to a display string. It never panics
// (recovering into a placeholder) and maps binary / out-of-band encodings to
// "<binary>" so a row preview never dumps opaque bytes.
func renderCell(ctx context.Context, td *val.TupleDesc, i int, tup val.Tuple) (out string) {
	defer func() {
		if r := recover(); r != nil {
			out = placeholderUnreadable
		}
	}()

	if i < 0 || i >= td.Count() {
		// Column not present in this tuple (e.g. a virtual/dropped column that
		// isn't materialized): degrade rather than index out of range.
		return placeholderUnreadable
	}
	if td.IsNull(i, tup) {
		return placeholderNull
	}

	switch td.Types[i].Enc {
	case val.ByteStringEnc, val.Hash128Enc, val.CellEnc,
		val.BytesAddrEnc, val.StringAddrEnc, val.JSONAddrEnc,
		val.GeomAddrEnc, val.CommitAddrEnc,
		val.BytesAdaptiveEnc, val.GeomAdaptiveEnc:
		return placeholderBinary
	}

	return td.FormatValue(ctx, i, td.GetField(i, tup))
}