From d7d944ffc8c41e13cb9ac5c87322004bea421b7e Mon Sep 17 00:00:00 2001 From: phuongddx <95doanphuong@gmail.com> Date: Sun, 26 Jul 2026 23:46:26 +0700 Subject: [PATCH 1/2] test: cover global_symbols.relationships round-trip Currently failing: insertGlobalSymbols never writes the relationships column, so the value is NULL for every symbol. --- cmd/scip/convert_test.go | 83 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/cmd/scip/convert_test.go b/cmd/scip/convert_test.go index af76f5c7..bef5cfb1 100644 --- a/cmd/scip/convert_test.go +++ b/cmd/scip/convert_test.go @@ -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" @@ -40,6 +41,7 @@ func TestConvert_SmokeTest(t *testing.T) { {"documents", checkDocuments}, {"symbols", checkSymbols}, {"occurrences", checkOccurrences}, + {"relationships", checkRelationships}, } for _, check := range checks { @@ -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}, }, }, { @@ -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") +} From 61751719c1d4791c301d6295f2d3f61e1dabc8e6 Mon Sep 17 00:00:00 2001 From: phuongddx <95doanphuong@gmail.com> Date: Sun, 26 Jul 2026 23:46:27 +0700 Subject: [PATCH 2/2] fix: populate global_symbols.relationships in expt-convert The relationships column was declared in the schema but never written, so every symbol reported no relationships while queries still succeeded -- making type-hierarchy features silently impossible to build on the SQLite output. Serialized like chunks.occurrences: a zstd-compressed wrapper message. Symbols without relationships bind NULL rather than an empty frame. Refs #464 --- cmd/scip/convert.go | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/cmd/scip/convert.go b/cmd/scip/convert.go index 50bfba18..4552c346 100644 --- a/cmd/scip/convert.go +++ b/cmd/scip/convert.go @@ -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 { @@ -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)