Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions cmd/scip/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,12 +420,33 @@ func (c *Converter) insertEnclosingRangeData(symbolToID map[string]int64, occs [
return nil
}

// marshalRelationships serializes a symbol's relationships for the
// global_symbols.relationships column, framed the same way Chunk.toDBFormat
// frames chunks.occurrences: a zstd-compressed wrapper message rather than a
// bare repeated field, so the blob stays self-describing.
func (c *Converter) marshalRelationships(rels []*scip.Relationship) ([]byte, error) {
blob, err := proto.Marshal(&scip.SymbolInformation{Relationships: rels})
if err != nil {
return nil, fmt.Errorf("failed to serialize relationships: %w", err)
}

var buf bytes.Buffer
c.zstdWriter.Reset(&buf)
if _, err = c.zstdWriter.Write(blob); err != nil {
return nil, fmt.Errorf("compression error: %w", err)
}
if err = c.zstdWriter.Close(); err != nil {
return nil, fmt.Errorf("flushing encoder: %w", err)
}
return buf.Bytes(), nil
}

func (c *Converter) insertGlobalSymbols(symbol *scip.SymbolInformation) (symbolID int64, err error) {
documentation := strings.Join(symbol.Documentation, "\n")

insertStmt, err := c.conn.Prepare(
`INSERT INTO global_symbols (symbol, display_name, kind, documentation, enclosing_symbol)
VALUES (?, ?, ?, ?, ?)
`INSERT INTO global_symbols (symbol, display_name, kind, documentation, enclosing_symbol, relationships)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(symbol) DO NOTHING
RETURNING id`)
if err != nil {
Expand Down Expand Up @@ -453,6 +474,19 @@ func (c *Converter) insertGlobalSymbols(symbol *scip.SymbolInformation) (symbolI
} else {
insertStmt.BindText(5, symbol.EnclosingSymbol)
}
// Bind NULL rather than an empty frame when there are no relationships:
// insertGlobalSymbols is also called with synthetic SymbolInformation
// values carrying only a Symbol, and a compressed empty message would make
// "no relationships" indistinguishable from "present but empty".
if len(symbol.Relationships) == 0 {
insertStmt.BindNull(6)
} else {
relationshipsBlob, err := c.marshalRelationships(symbol.Relationships)
if err != nil {
return 0, err
}
insertStmt.BindBytes(6, relationshipsBlob)
}

if _, err = insertStmt.Step(); err != nil {
return 0, fmt.Errorf("failed to insert symbol %s: %w", symbol.Symbol, err)
Expand Down
83 changes: 82 additions & 1 deletion cmd/scip/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/klauspost/compress/zstd"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"

Expand Down Expand Up @@ -40,6 +41,7 @@ func TestConvert_SmokeTest(t *testing.T) {
{"documents", checkDocuments},
{"symbols", checkSymbols},
{"occurrences", checkOccurrences},
{"relationships", checkRelationships},
}

for _, check := range checks {
Expand All @@ -51,15 +53,24 @@ func TestConvert_SmokeTest(t *testing.T) {

func testIndex1() *scip.Index {
pkg1S1Sym := "scip-go go . . pkg1/S1#"
pkg1I1Sym := "scip-go go . . pkg1/I1#"
return &scip.Index{
Documents: []*scip.Document{
{
RelativePath: "a.go",
Occurrences: []*scip.Occurrence{
{Symbol: pkg1S1Sym, Range: []int32{10, 3, 6}, SymbolRoles: int32(scip.SymbolRole_Definition)},
{Symbol: pkg1I1Sym, Range: []int32{20, 3, 6}, SymbolRoles: int32(scip.SymbolRole_Definition)},
},
Symbols: []*scip.SymbolInformation{
{Symbol: pkg1S1Sym},
// S1 implements I1 — exercises the relationships column.
{
Symbol: pkg1S1Sym,
Relationships: []*scip.Relationship{
{Symbol: pkg1I1Sym, IsImplementation: true},
},
},
{Symbol: pkg1I1Sym},
},
},
{
Expand Down Expand Up @@ -173,3 +184,73 @@ type occurrenceData struct {
Role int32
Range scip.Range
}

// checkRelationships asserts SymbolInformation.relationships survives the
// round-trip into global_symbols.relationships, and that symbols without
// relationships store NULL rather than a compressed empty message. The column
// is declared in the schema, so a converter that never writes it yields a
// database that queries cleanly while reporting every symbol as having no
// relationships.
func checkRelationships(t *testing.T, index *scip.Index, db *sqlite.Conn) {
expected := map[string][]*scip.Relationship{}
for _, doc := range index.Documents {
for _, sym := range doc.Symbols {
if len(sym.Relationships) > 0 {
expected[sym.Symbol] = sym.Relationships
}
}
}
require.NotEmpty(t, expected, "fixture must exercise at least one relationship")

decoder, err := zstd.NewReader(nil)
require.NoError(t, err)
defer decoder.Close()

found := map[string][]*scip.Relationship{}
query := "SELECT symbol, relationships FROM global_symbols WHERE relationships IS NOT NULL"
err = sqlitex.ExecuteTransient(db, query, &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
symbol := stmt.ColumnText(0)
blob := make([]byte, stmt.ColumnLen(1))
stmt.ColumnBytes(1, blob)

raw, err := decoder.DecodeAll(blob, nil)
if err != nil {
return err
}
var info scip.SymbolInformation
if err := proto.Unmarshal(raw, &info); err != nil {
return err
}
found[symbol] = info.Relationships
return nil
},
})
require.NoError(t, err)

require.Len(t, found, len(expected))
for symbol, want := range expected {
got, ok := found[symbol]
require.True(t, ok, "no relationships stored for %s", symbol)
require.Len(t, got, len(want))
for i := range want {
require.Equal(t, want[i].Symbol, got[i].Symbol)
require.Equal(t, want[i].IsImplementation, got[i].IsImplementation)
}
}

// Symbols carrying no relationships must be NULL, not an empty frame:
// insertGlobalSymbols is also called with synthetic SymbolInformation
// values that only have a Symbol.
var nullCount int
err = sqlitex.ExecuteTransient(db,
"SELECT COUNT(*) FROM global_symbols WHERE relationships IS NULL",
&sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
nullCount = stmt.ColumnInt(0)
return nil
},
})
require.NoError(t, err)
require.Greater(t, nullCount, 0, "symbols without relationships must store NULL")
}