@@ 275,6 275,30 @@ func TestRowsEmptyTable(t *testing.T) {
}
}
+// TestRowsResolvesLongText guards the fix for out-of-line (address-encoded)
+// text: a >4KB longtext column must render its actual content, not the
+// "<binary>" placeholder that inline-only rendering produced.
+func TestRowsResolvesLongText(t *testing.T) {
+ db := openFixture(t)
+ ctx := context.Background()
+
+ page, err := db.Rows(ctx, "main", "docs", 0, 10)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(page.Rows) != 1 {
+ t.Fatalf("docs rows = %d, want 1", len(page.Rows))
+ }
+ body := page.Rows[0][1] // columns: id, body
+ if body == placeholderBinary {
+ t.Fatalf("longtext rendered as %q; address-encoded text was not resolved", body)
+ }
+ if body != longDocBody {
+ t.Errorf("longtext body mismatch: got %d bytes, want %d bytes (prefix %.40q)",
+ len(body), len(longDocBody), body)
+ }
+}
+
func TestCommitSummaryAddTable(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
@@ 36,9 36,15 @@ const (
msgAddUser = "add users" // C1: create empty users table
msgInsert = "insert users" // C2: insert 3 rows
msgModify = "modify users" // C3: update 1 row, insert 1 row
- msgAddItem = "add items" // C4: create empty items table
+ msgAddItem = "add items" // C4: create empty items + docs tables
)
+// longDocBody is a >4KB longtext value; at this size Dolt stores the string
+// out-of-line (address-encoded) rather than inline, exercising the browse
+// layer's address-resolution path (a short string would stay inline and never
+// hit it).
+var longDocBody = strings.Repeat("The quick brown fox. ", 300) // 6300 bytes
+
var (
haveDolt bool
fixtureStore string // path to the bare store, main head = C4
@@ 142,7 148,14 @@ func buildFixture(root string) (string, error) {
{"branch", "dev"}, // dev head = C2
{"sql", "-q", "update users set name='alicia' where id=1; insert into users values (4,'dave')"},
{"commit", "-Am", msgModify},
+ // C4 adds the empty items table and, in the same commit, a docs table
+ // carrying one >4KB longtext row — the out-of-line-text fixture. Folding
+ // it into C4 keeps the commit topology (and every commit-count assertion)
+ // unchanged; TestTables looks tables up by name, so the extra table is
+ // invisible to it while TestRowsResolvesLongText reads docs directly.
{"sql", "-q", "create table items (sku varchar(16) primary key, qty int)"},
+ {"sql", "-q", "create table docs (id int primary key, body longtext)"},
+ {"sql", "-q", fmt.Sprintf("insert into docs values (1, '%s')", longDocBody)},
{"commit", "-Am", msgAddItem},
{"push", "origin", "main"},
{"push", "origin", "dev"},
@@ 8,6 8,7 @@ import (
"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/prolly/tree"
"github.com/dolthub/dolt/go/store/val"
)
@@ 190,6 191,9 @@ func (db *DB) Rows(ctx context.Context, refStr, table string, offset, limit int)
return nil, fmt.Errorf("browse: iterate rows of %q: %w", table, err)
}
+ // ns dereferences out-of-line (address-encoded) values — text/longtext that
+ // Dolt stores in a separate chunk rather than inline in the tuple.
+ ns := m.NodeStore()
for {
key, value, err := iter.Next(ctx)
if err == io.EOF {
@@ 202,9 206,9 @@ func (db *DB) Rows(ctx context.Context, refStr, table string, offset, limit int)
row := make([]string, len(refs))
for i, r := range refs {
if r.fromKey {
- row[i] = renderCell(ctx, keyDesc, r.idx, key)
+ row[i] = renderCell(ctx, ns, keyDesc, r.idx, key)
} else {
- row[i] = renderCell(ctx, valDesc, r.idx, value)
+ row[i] = renderCell(ctx, ns, valDesc, r.idx, value)
}
}
page.Rows = append(page.Rows, row)
@@ 251,7 255,14 @@ func rowLayout(sch schema.Schema) ([]string, []cellRef) {
// 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) {
+//
+// text/longtext columns are the exception: Dolt stores anything past a small
+// inline threshold out-of-line, addressed by a content hash (StringAddrEnc, or
+// StringAdaptiveEnc which is inline-or-address in the same field). Those are
+// resolved through ns to their real content — without this they would render as
+// "<binary>" (addr) or a raw hash string (adaptive), losing every long
+// description, close reason, comment body, and audit payload.
+func renderCell(ctx context.Context, ns tree.NodeStore, td *val.TupleDesc, i int, tup val.Tuple) (out string) {
defer func() {
if r := recover(); r != nil {
out = placeholderUnreadable
@@ 268,12 279,56 @@ func renderCell(ctx context.Context, td *val.TupleDesc, i int, tup val.Tuple) (o
}
switch td.Types[i].Enc {
+ case val.StringAddrEnc:
+ return resolveStringAddr(ctx, ns, td, i, tup)
+ case val.StringAdaptiveEnc:
+ return resolveStringAdaptive(ctx, ns, td, i, tup)
case val.ByteStringEnc, val.Hash128Enc, val.CellEnc,
- val.BytesAddrEnc, val.StringAddrEnc, val.JSONAddrEnc,
+ val.BytesAddrEnc, val.JSONAddrEnc,
val.GeomAddrEnc, val.CommitAddrEnc,
val.BytesAdaptiveEnc, val.GeomAdaptiveEnc:
+ // Genuine binary / opaque out-of-band values: never dump raw bytes.
return placeholderBinary
}
return td.FormatValue(ctx, i, td.GetField(i, tup))
}
+
+// resolveStringAddr dereferences a StringAddrEnc field (text/longtext always
+// stored out-of-line) to its full content.
+func resolveStringAddr(ctx context.Context, ns tree.NodeStore, td *val.TupleDesc, i int, tup val.Tuple) string {
+ h, ok := td.GetStringAddr(i, tup)
+ if !ok {
+ return placeholderNull
+ }
+ s, err := val.NewTextStorage(h, ns).Unwrap(ctx)
+ if err != nil {
+ return placeholderUnreadable
+ }
+ return s
+}
+
+// resolveStringAdaptive reads a StringAdaptiveEnc field, which stores its value
+// either inline (returned as a string) or out-of-line (returned as a
+// *val.TextStorage to Unwrap) in the same field.
+func resolveStringAdaptive(ctx context.Context, ns tree.NodeStore, td *val.TupleDesc, i int, tup val.Tuple) string {
+ v, ok, err := td.GetStringAdaptiveValue(ctx, i, ns, tup)
+ if err != nil || !ok {
+ if err != nil {
+ return placeholderUnreadable
+ }
+ return placeholderNull
+ }
+ switch s := v.(type) {
+ case string:
+ return s
+ case *val.TextStorage:
+ str, err := s.Unwrap(ctx)
+ if err != nil {
+ return placeholderUnreadable
+ }
+ return str
+ default:
+ return placeholderUnreadable
+ }
+}