a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
253 lines
8.1 KiB
Go
253 lines
8.1 KiB
Go
package store_sqlite
|
|
|
|
import (
|
|
"database/sql"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// TestPromotedColumns_RoundTrip verifies the promoted keys land in their
|
|
// columns, are stripped from the JSON blob, and restore into Meta with
|
|
// exact types — while non-promoted keys stay in the blob.
|
|
func TestPromotedColumns_RoundTrip(t *testing.T) {
|
|
s, err := Open(filepath.Join(t.TempDir(), "p.sqlite"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
s.AddNode(&graph.Node{
|
|
ID: "f.go::F", Kind: graph.KindFunction, Name: "F", FilePath: "f.go",
|
|
Meta: map[string]any{
|
|
"signature": "func F()",
|
|
"visibility": "public",
|
|
"doc": "F docs",
|
|
"external": true,
|
|
"complexity": 5, // non-promoted — must stay in the blob
|
|
},
|
|
})
|
|
|
|
n := s.GetNode("f.go::F")
|
|
if n == nil {
|
|
t.Fatal("GetNode returned nil")
|
|
}
|
|
assertType[string](t, n.Meta, "signature", "func F()")
|
|
assertType[string](t, n.Meta, "visibility", "public")
|
|
assertType[string](t, n.Meta, "doc", "F docs")
|
|
assertType[bool](t, n.Meta, "external", true)
|
|
assertType[int](t, n.Meta, "complexity", 5)
|
|
|
|
var sig, vis, doc sql.NullString
|
|
var ext sql.NullBool
|
|
var blob []byte
|
|
row := s.db.QueryRow(`SELECT signature, visibility, doc, external, meta FROM nodes WHERE id=?`, "f.go::F")
|
|
if err := row.Scan(&sig, &vis, &doc, &ext, &blob); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !sig.Valid || sig.String != "func F()" {
|
|
t.Errorf("signature column = %+v", sig)
|
|
}
|
|
if !ext.Valid || !ext.Bool {
|
|
t.Errorf("external column = %+v", ext)
|
|
}
|
|
blobStr := string(blob)
|
|
for _, k := range []string{"signature", "visibility", "external"} {
|
|
if strings.Contains(blobStr, k) {
|
|
t.Errorf("blob still contains promoted key %q: %s", k, blobStr)
|
|
}
|
|
}
|
|
if !strings.Contains(blobStr, "complexity") {
|
|
t.Errorf("blob missing non-promoted key complexity: %s", blobStr)
|
|
}
|
|
}
|
|
|
|
// TestPromotedColumns_NewColumns verifies the added promoted meta columns
|
|
// (is_async / is_static / is_abstract / is_exported / return_type / updated_at)
|
|
// and the struct-field columns (start_column / end_column) round-trip through
|
|
// their typed columns, and that a SQL filter on is_async resolves WITHOUT
|
|
// decoding the meta blob — the indexable-column acceptance.
|
|
func TestPromotedColumns_NewColumns(t *testing.T) {
|
|
s, err := Open(filepath.Join(t.TempDir(), "p.sqlite"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
s.AddNode(&graph.Node{
|
|
ID: "f.go::Async", Kind: graph.KindFunction, Name: "Async", FilePath: "f.go",
|
|
StartLine: 10, EndLine: 20, StartColumn: 4, EndColumn: 1,
|
|
Meta: map[string]any{
|
|
"is_async": true,
|
|
"is_static": false,
|
|
"is_abstract": true,
|
|
"is_exported": true,
|
|
"return_type": "error",
|
|
"updated_at": int64(1700000000),
|
|
"complexity": 3, // non-promoted — stays in the blob
|
|
},
|
|
})
|
|
// A second, non-async node to prove the filter is selective.
|
|
s.AddNode(&graph.Node{
|
|
ID: "f.go::Sync", Kind: graph.KindFunction, Name: "Sync", FilePath: "f.go",
|
|
Meta: map[string]any{"is_async": false},
|
|
})
|
|
|
|
// Read-back restores every promoted key into Meta with its exact type,
|
|
// and the struct columns into the Node fields.
|
|
n := s.GetNode("f.go::Async")
|
|
if n == nil {
|
|
t.Fatal("GetNode returned nil")
|
|
}
|
|
assertType[bool](t, n.Meta, "is_async", true)
|
|
assertType[bool](t, n.Meta, "is_static", false)
|
|
assertType[bool](t, n.Meta, "is_abstract", true)
|
|
assertType[bool](t, n.Meta, "is_exported", true)
|
|
assertType[string](t, n.Meta, "return_type", "error")
|
|
assertType[int64](t, n.Meta, "updated_at", int64(1700000000))
|
|
assertType[int](t, n.Meta, "complexity", 3)
|
|
if n.StartColumn != 4 || n.EndColumn != 1 {
|
|
t.Errorf("column offsets = (%d,%d), want (4,1)", n.StartColumn, n.EndColumn)
|
|
}
|
|
|
|
// The promoted keys are stripped from the JSON blob; complexity is not.
|
|
var blob []byte
|
|
if err := s.db.QueryRow(`SELECT meta FROM nodes WHERE id=?`, "f.go::Async").Scan(&blob); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, k := range []string{"is_async", "is_abstract", "return_type", "updated_at"} {
|
|
if strings.Contains(string(blob), k) {
|
|
t.Errorf("blob still contains promoted key %q: %s", k, blob)
|
|
}
|
|
}
|
|
if !strings.Contains(string(blob), "complexity") {
|
|
t.Errorf("blob missing non-promoted key complexity: %s", blob)
|
|
}
|
|
|
|
// Acceptance: a SQL filter on the typed column resolves the node without
|
|
// touching the meta blob (only id is selected).
|
|
var id string
|
|
var startCol, endCol int
|
|
if err := s.db.QueryRow(
|
|
`SELECT id, start_column, end_column FROM nodes WHERE is_async = 1`,
|
|
).Scan(&id, &startCol, &endCol); err != nil {
|
|
t.Fatalf("is_async column filter failed: %v", err)
|
|
}
|
|
if id != "f.go::Async" || startCol != 4 || endCol != 1 {
|
|
t.Errorf("filter result = (%q,%d,%d), want (f.go::Async,4,1)", id, startCol, endCol)
|
|
}
|
|
}
|
|
|
|
// TestPromotedColumns_SemanticType verifies semantic_type/semantic_source —
|
|
// promoted alongside signature/visibility/etc. so enrichment can query the
|
|
// unstamped subset by column instead of decoding every node's meta blob —
|
|
// round-trip through their columns and out of the JSON blob.
|
|
func TestPromotedColumns_SemanticType(t *testing.T) {
|
|
s, err := Open(filepath.Join(t.TempDir(), "p.sqlite"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
s.AddNode(&graph.Node{
|
|
ID: "f.go::T", Kind: graph.KindFunction, Name: "T", FilePath: "f.go",
|
|
Meta: map[string]any{
|
|
"semantic_type": "string",
|
|
"semantic_source": "lsp-gopls",
|
|
"complexity": 5, // non-promoted — must stay in the blob
|
|
},
|
|
})
|
|
|
|
n := s.GetNode("f.go::T")
|
|
if n == nil {
|
|
t.Fatal("GetNode returned nil")
|
|
}
|
|
assertType[string](t, n.Meta, "semantic_type", "string")
|
|
assertType[string](t, n.Meta, "semantic_source", "lsp-gopls")
|
|
assertType[int](t, n.Meta, "complexity", 5)
|
|
|
|
var st, ss sql.NullString
|
|
var blob []byte
|
|
row := s.db.QueryRow(`SELECT semantic_type, semantic_source, meta FROM nodes WHERE id=?`, "f.go::T")
|
|
if err := row.Scan(&st, &ss, &blob); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !st.Valid || st.String != "string" {
|
|
t.Errorf("semantic_type column = %+v", st)
|
|
}
|
|
if !ss.Valid || ss.String != "lsp-gopls" {
|
|
t.Errorf("semantic_source column = %+v", ss)
|
|
}
|
|
blobStr := string(blob)
|
|
for _, k := range []string{"semantic_type", "semantic_source"} {
|
|
if strings.Contains(blobStr, k) {
|
|
t.Errorf("blob still contains promoted key %q: %s", k, blobStr)
|
|
}
|
|
}
|
|
if !strings.Contains(blobStr, "complexity") {
|
|
t.Errorf("blob missing non-promoted key complexity: %s", blobStr)
|
|
}
|
|
}
|
|
|
|
// TestPromotedColumns_ExternalFalse guards the NULL-vs-false distinction:
|
|
// a stored false must round-trip as false, not vanish.
|
|
func TestPromotedColumns_ExternalFalse(t *testing.T) {
|
|
s, err := Open(filepath.Join(t.TempDir(), "p.sqlite"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
s.AddNode(&graph.Node{
|
|
ID: "x", Kind: graph.KindFunction, Name: "x", FilePath: "x.go",
|
|
Meta: map[string]any{"external": false},
|
|
})
|
|
n := s.GetNode("x")
|
|
if n == nil {
|
|
t.Fatal("nil")
|
|
}
|
|
v, ok := n.Meta["external"].(bool)
|
|
if !ok || v != false {
|
|
t.Errorf("external false: got %v (%T)", n.Meta["external"], n.Meta["external"])
|
|
}
|
|
}
|
|
|
|
// TestPromotedColumns_Migration verifies ensureNodeColumns adds the
|
|
// promoted columns to a database created with the pre-promotion schema.
|
|
func TestPromotedColumns_Migration(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "old.sqlite")
|
|
raw, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = raw.Exec(`CREATE TABLE nodes (
|
|
id TEXT PRIMARY KEY, kind TEXT NOT NULL, name TEXT NOT NULL,
|
|
qual_name TEXT NOT NULL DEFAULT '', file_path TEXT NOT NULL,
|
|
start_line INTEGER NOT NULL DEFAULT 0, end_line INTEGER NOT NULL DEFAULT 0,
|
|
language TEXT NOT NULL DEFAULT '', repo_prefix TEXT NOT NULL DEFAULT '',
|
|
workspace_id TEXT NOT NULL DEFAULT '', project_id TEXT NOT NULL DEFAULT '',
|
|
meta BLOB
|
|
) WITHOUT ROWID`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = raw.Close()
|
|
|
|
s, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("Open old-schema db: %v", err)
|
|
}
|
|
defer s.Close()
|
|
s.AddNode(&graph.Node{
|
|
ID: "m", Kind: graph.KindFunction, Name: "m", FilePath: "m.go",
|
|
Meta: map[string]any{"signature": "sig", "external": true},
|
|
})
|
|
n := s.GetNode("m")
|
|
if n == nil {
|
|
t.Fatal("nil after migration")
|
|
}
|
|
assertType[string](t, n.Meta, "signature", "sig")
|
|
assertType[bool](t, n.Meta, "external", true)
|
|
}
|