~bigbes/sr-ht-dolt

ref: a1fe404f916f0a66242954c9843115ef7146584c sr-ht-dolt/mcpsrv/browse_nulls_test.go -rw-r--r-- 3.8 KiB
a1fe404f — Eugene Blikh docs: spell out the clipped-read answers 5 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
package mcpsrv_test

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
	"sourcecraft.dev/bigbes/sr-ht-dolt/core"
	"sourcecraft.dev/bigbes/sr-ht-dolt/mcpsrv"
)

// read_rows and the one question a rendered string cannot answer: is there a
// value in this cell at all?
//
// browse renders a real NULL as the text "NULL", which is what a row storing
// those four characters renders as too. On a page the two are interchangeable —
// a reader sees the same thing either way — but this tool hands rows to a
// machine with no schema beside them, and an agent cannot recover the
// distinction afterwards. So the mask browse carries beside its rows is spent
// here: an absent value is JSON null, a stored "NULL" is the string.

// nullsTable is a table holding both halves of the ambiguity in one column:
// row 2 stores no value, row 3 stores the text "NULL", and browse renders both
// as "NULL". Only the mask tells them apart.
func nullsTable() fakeTable {
	return fakeTable{
		name: "cells",
		cols: []browse.ColumnInfo{
			{Name: "id", Type: "int", PrimaryKey: true},
			{Name: "body", Type: "text", Nullable: true},
		},
		rows: [][]string{
			{"1", "plain"},
			{"2", "NULL"}, // no value: browse rendered the placeholder
			{"3", "NULL"}, // the four characters, actually stored
		},
		nulls: [][]bool{
			{false, false},
			{false, true},
			{false, false},
		},
	}
}

// nullableRowsResult is read_rows decoded into cells that can be null, which is
// the shape this tool answers: [][]*string, where a nil cell is a JSON null.
type nullableRowsResult struct {
	Columns []string    `json:"columns"`
	Rows    [][]*string `json:"rows"`
}

func TestReadRowsAnswersARealNullAsNullAndAStoredNullAsAString(t *testing.T) {
	res := call(t, connect(t, nullsSurface(t), nil), "read_rows", args("cells", "table", "cells"))
	require.False(t, res.IsError, "%s", errorText(res))

	var out nullableRowsResult
	decode(t, res, &out)

	require.Equal(t, []string{"id", "body"}, out.Columns)
	require.Len(t, out.Rows, 3)

	require.NotNil(t, out.Rows[0][1])
	assert.Equal(t, "plain", *out.Rows[0][1])

	assert.Nil(t, out.Rows[1][1], "a cell that holds no value is null, not the string \"NULL\"")

	require.NotNil(t, out.Rows[2][1], "a row that stores the text \"NULL\" still has a value")
	assert.Equal(t, "NULL", *out.Rows[2][1])

	// A key is never null, and neither is a cell that holds an ordinary value:
	// the mask is spent on the one column that needed it and nowhere else.
	for i, row := range out.Rows {
		require.NotNilf(t, row[0], "row %d: the primary key holds a value", i)
	}
}

// The distinction has to survive the wire, not merely the Go struct: what an
// agent reads is the JSON, so the null is asserted there too.
func TestReadRowsPutsANullOnTheWire(t *testing.T) {
	res := call(t, connect(t, nullsSurface(t), nil), "read_rows", args("cells", "table", "cells"))
	require.False(t, res.IsError, "%s", errorText(res))
	require.NotNil(t, res.StructuredContent)

	raw, err := json.Marshal(res.StructuredContent)
	require.NoError(t, err)
	assert.Contains(t, string(raw), `[["1","plain"],["2",null],["3","NULL"]]`,
		"the absent value is a JSON null and the stored text is a string")
}

// nullsSurface is the server over the cells database.
func nullsSurface(t *testing.T) *mcpsrv.Server {
	t.Helper()

	repos := &fakeRepos{acl: map[int]map[int]core.AccessMode{}}
	repos.repos = append(repos.repos, &core.Repo{
		ID:         1,
		Name:       "cells",
		OwnerID:    aliceID,
		OwnerName:  "alice",
		Path:       storePath("alice", "cells"),
		Visibility: core.VisibilityPublic,
	})

	opener := &fakeOpener{sessions: map[string]*fakeSession{
		storePath("alice", "cells"): smallStore("main", "cccc", []fakeTable{nullsTable()}),
	}}

	return newServer(t, repos, opener)
}