Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

501 lines
15 KiB
Go

package store_sqlite
// This file implements the moderate-SQL analysis capability interfaces
// for the SQLite graph.Store backend. Each method mirrors the in-memory
// reference implementation in internal/graph/graph.go and is verified
// against the same conformance suite (internal/graph/storetest).
//
// Shape: push the structural filter into one indexed SELECT via the raw-
// SQL helpers (queryNodesSQL / s.db.Query), then do any Meta-dependent
// (JSON-decoded) or distinct-counting filtering in Go. No new prepared
// statements are added — every query rides the secondary indexes already
// created in schema.go (edges_by_from / edges_by_to / nodes_by_kind).
import (
"github.com/zzet/gortex/internal/graph"
)
// Compile-time assertions: *Store satisfies each analysis capability.
var _ graph.DeadCodeCandidator = (*Store)(nil)
var _ graph.IfaceImplementsScanner = (*Store)(nil)
var _ graph.MemberMethodsByType = (*Store)(nil)
var _ graph.StructuralParentEdges = (*Store)(nil)
var _ graph.ExtractCandidatesScanner = (*Store)(nil)
var _ graph.CrossRepoCandidates = (*Store)(nil)
var _ graph.ThrowerErrorSurfacer = (*Store)(nil)
// anaDedupeEdgeKinds drops empty / duplicate edge kinds, preserving
// first-seen order — the EdgeKind twin of dedupeNonEmpty.
func anaDedupeEdgeKinds(in []graph.EdgeKind) []graph.EdgeKind {
seen := make(map[graph.EdgeKind]struct{}, len(in))
out := make([]graph.EdgeKind, 0, len(in))
for _, k := range in {
if k == "" {
continue
}
if _, ok := seen[k]; ok {
continue
}
seen[k] = struct{}{}
out = append(out, k)
}
return out
}
// --- DeadCodeCandidator -------------------------------------------------
// DeadCodeCandidates returns nodes of the allowed kinds that have no
// incoming edge of the corresponding allowed in-edge kinds. An empty
// per-kind allowlist (or one that dedupes to nothing) means "any incoming
// edge counts as usage". Mirrors graph.(*Graph).DeadCodeCandidates: the
// candidate set is purely structural (the analysis layer applies the
// exported / test / entry-point / synthetic post-filters in Go), so no
// node-id exclusion happens here. The NOT-EXISTS filter runs server-side
// per node kind.
func (s *Store) DeadCodeCandidates(allowedNodeKinds []graph.NodeKind, allowedInEdgeKinds map[graph.NodeKind][]graph.EdgeKind) []*graph.Node {
if len(allowedNodeKinds) == 0 {
return nil
}
var out []*graph.Node
for _, nk := range allowedNodeKinds {
allowed := anaDedupeEdgeKinds(allowedInEdgeKinds[nk])
anyKindCounts := len(allowed) == 0
var q string
var args []any
if anyKindCounts {
// Any incoming edge disqualifies the node.
q = `SELECT ` + lookupNodeCols + ` FROM nodes n
WHERE n.kind = ?
AND NOT EXISTS (SELECT 1 FROM edges e WHERE e.to_id = n.id)
ORDER BY n.id`
args = []any{string(nk)}
} else {
// Only an incoming edge of one of the allowed kinds counts.
q = `SELECT ` + lookupNodeCols + ` FROM nodes n
WHERE n.kind = ?
AND NOT EXISTS (SELECT 1 FROM edges e WHERE e.to_id = n.id AND e.kind IN (` + inPlaceholders(len(allowed)) + `))
ORDER BY n.id`
args = make([]any, 0, 1+len(allowed))
args = append(args, string(nk))
for _, ek := range allowed {
args = append(args, string(ek))
}
}
for _, n := range s.queryNodesSQL(q, args...) {
if n != nil {
out = append(out, n)
}
}
}
return out
}
// --- IfaceImplementsScanner ---------------------------------------------
// IfaceImplementsRows returns one row per EdgeImplements edge whose
// target is a KindInterface carrying Meta["methods"]. The interface's
// decoded Meta rides on the row (callers pull the "methods" field, which
// round-trips as []string or []any). Interfaces with no Meta or no
// "methods" key are elided server-side.
func (s *Store) IfaceImplementsRows() []graph.IfaceImplementsRow {
q := `SELECT e.from_id, n.id, n.meta
FROM edges e
JOIN nodes n ON n.id = e.to_id
WHERE e.kind = ? AND n.kind = ? AND n.meta IS NOT NULL`
rows, err := s.db.Query(q, string(graph.EdgeImplements), string(graph.KindInterface))
if err != nil {
return nil
}
defer rows.Close()
var out []graph.IfaceImplementsRow
for rows.Next() {
var fromID, ifaceID string
var metaBlob []byte
if err := rows.Scan(&fromID, &ifaceID, &metaBlob); err != nil {
continue
}
meta, derr := decodeMeta(metaBlob)
if derr != nil || meta == nil {
continue
}
if _, ok := meta["methods"]; !ok {
continue
}
out = append(out, graph.IfaceImplementsRow{
TypeID: fromID,
IfaceID: ifaceID,
IfaceMeta: meta,
})
}
return out
}
// --- MemberMethodsByType ------------------------------------------------
// MemberMethodsByType returns typeID → []MemberMethodInfo for every
// EdgeMemberOf edge whose source is a KindMethod. The columns come from
// the METHOD NODE (FilePath / StartLine / RepoPrefix), matching the
// in-memory reference. Per-type lists are deduplicated by MethodID; the
// scan is ordered by the edge PK so the first-seen winner is stable. An
// empty graph (no qualifying rows) returns nil.
func (s *Store) MemberMethodsByType() map[string][]graph.MemberMethodInfo {
q := `SELECT e.to_id, n.id, n.name, n.file_path, n.start_line, n.repo_prefix
FROM edges e
JOIN nodes n ON n.id = e.from_id
WHERE e.kind = ? AND n.kind = ?
ORDER BY e.id`
rows, err := s.db.Query(q, string(graph.EdgeMemberOf), string(graph.KindMethod))
if err != nil {
return nil
}
defer rows.Close()
out := make(map[string][]graph.MemberMethodInfo)
seen := make(map[string]map[string]struct{})
for rows.Next() {
var typeID, methodID, name, filePath, repoPrefix string
var startLine int
if err := rows.Scan(&typeID, &methodID, &name, &filePath, &startLine, &repoPrefix); err != nil {
continue
}
if seen[typeID] == nil {
seen[typeID] = make(map[string]struct{})
}
if _, ok := seen[typeID][methodID]; ok {
continue
}
seen[typeID][methodID] = struct{}{}
out[typeID] = append(out[typeID], graph.MemberMethodInfo{
MethodID: methodID,
Name: name,
FilePath: filePath,
StartLine: startLine,
RepoPrefix: repoPrefix,
})
}
if len(out) == 0 {
// Match the in-memory reference: empty graph returns nil.
return nil
}
return out
}
// --- StructuralParentEdges ----------------------------------------------
// StructuralParentEdges returns every Extends / Implements / Composes
// edge whose endpoints are both Type / Interface, projected as (FromID,
// ToID, FromKind, ToKind, Origin). Endpoints that aren't both type /
// interface are filtered server-side. Empty graph or no matching edges
// returns nil.
func (s *Store) StructuralParentEdges() []graph.StructuralParentEdgeRow {
q := `SELECT e.from_id, e.to_id, nf.kind, nt.kind, e.origin
FROM edges e
JOIN nodes nf ON nf.id = e.from_id
JOIN nodes nt ON nt.id = e.to_id
WHERE e.kind IN (?,?,?)
AND nf.kind IN (?,?) AND nt.kind IN (?,?)
ORDER BY e.id`
rows, err := s.db.Query(q,
string(graph.EdgeExtends), string(graph.EdgeImplements), string(graph.EdgeComposes),
string(graph.KindType), string(graph.KindInterface),
string(graph.KindType), string(graph.KindInterface),
)
if err != nil {
return nil
}
defer rows.Close()
var out []graph.StructuralParentEdgeRow
for rows.Next() {
var fromID, toID, fromKind, toKind, origin string
if err := rows.Scan(&fromID, &toID, &fromKind, &toKind, &origin); err != nil {
continue
}
out = append(out, graph.StructuralParentEdgeRow{
FromID: fromID,
ToID: toID,
FromKind: graph.NodeKind(fromKind),
ToKind: graph.NodeKind(toKind),
Origin: origin,
})
}
return out
}
// --- ExtractCandidatesScanner -------------------------------------------
// ExtractCandidates ranks function / method nodes by extractability: line
// span (EndLine - StartLine + 1), distinct caller fan-in, and distinct
// callee fan-out, counting only edges whose kind is in the supplied set.
// Rows must clear all three thresholds. Nodes with a zero StartLine /
// EndLine are dropped; pathPrefix narrows by file-path prefix. Mirrors
// graph.(*Graph).ExtractCandidates exactly: only KindFunction +
// KindMethod nodes are considered, and the distinct-by-endpoint counting
// runs Go-side over GetInEdges / GetOutEdges.
func (s *Store) ExtractCandidates(kinds []graph.EdgeKind, minLines, minCallers, minFanOut int, pathPrefix string) []graph.ExtractCandidateRow {
if len(kinds) == 0 {
return nil
}
kindSet := make(map[graph.EdgeKind]struct{}, len(kinds))
for _, k := range kinds {
if k == "" {
continue
}
kindSet[k] = struct{}{}
}
if len(kindSet) == 0 {
return nil
}
// Candidate nodes: function / method only, non-zero line span,
// optional path-prefix gate.
q := `SELECT ` + lookupNodeCols + ` FROM nodes
WHERE kind IN (?,?) AND start_line > 0 AND end_line > 0`
args := []any{string(graph.KindFunction), string(graph.KindMethod)}
if pathPrefix != "" {
q += ` AND file_path LIKE ? ESCAPE '\'`
args = append(args, escapeLikePattern(pathPrefix)+"%")
}
q += ` ORDER BY id`
nodes := s.queryNodesSQL(q, args...)
var out []graph.ExtractCandidateRow
for _, n := range nodes {
if n == nil {
continue
}
lineCount := n.EndLine - n.StartLine + 1
if lineCount < minLines {
continue
}
callerSet := make(map[string]struct{})
for _, e := range s.GetInEdges(n.ID) {
if e == nil {
continue
}
if _, ok := kindSet[e.Kind]; !ok {
continue
}
callerSet[e.From] = struct{}{}
}
if len(callerSet) < minCallers {
continue
}
calleeSet := make(map[string]struct{})
for _, e := range s.GetOutEdges(n.ID) {
if e == nil {
continue
}
if _, ok := kindSet[e.Kind]; !ok {
continue
}
calleeSet[e.To] = struct{}{}
}
if len(calleeSet) < minFanOut {
continue
}
out = append(out, graph.ExtractCandidateRow{
NodeID: n.ID,
Name: n.Name,
FilePath: n.FilePath,
StartLine: n.StartLine,
EndLine: n.EndLine,
LineCount: lineCount,
CallerCount: len(callerSet),
FanOut: len(calleeSet),
})
}
return out
}
// --- CrossRepoCandidates ------------------------------------------------
// CrossRepoCandidates returns every edge whose kind is in baseKinds and
// whose endpoints carry two different non-empty RepoPrefix values. The
// edge is returned verbatim (callers rewrite Edge.CrossRepo); FromRepo /
// ToRepo are the endpoint prefixes. Empty baseKinds returns nil; single-
// repo graphs (or graphs whose nodes carry no RepoPrefix) yield nothing.
func (s *Store) CrossRepoCandidates(baseKinds []graph.EdgeKind) []graph.CrossRepoCandidateRow {
uniq := anaDedupeEdgeKinds(baseKinds)
if len(uniq) == 0 {
return nil
}
args := make([]any, 0, len(uniq))
for _, k := range uniq {
args = append(args, string(k))
}
q := `SELECT e.from_id, e.to_id, e.kind, e.file_path, e.line,
e.confidence, e.confidence_label, e.origin, e.tier, e.cross_repo, e.meta,
nf.repo_prefix, nt.repo_prefix
FROM edges e
JOIN nodes nf ON nf.id = e.from_id
JOIN nodes nt ON nt.id = e.to_id
WHERE e.kind IN (` + inPlaceholders(len(uniq)) + `)
AND nf.repo_prefix <> '' AND nt.repo_prefix <> ''
AND nf.repo_prefix <> nt.repo_prefix
ORDER BY e.id`
rows, err := s.db.Query(q, args...)
if err != nil {
return nil
}
defer rows.Close()
var out []graph.CrossRepoCandidateRow
for rows.Next() {
var (
fromRepo, toRepo string
e graph.Edge
metaBlob []byte
crossRepo int64
)
if err := rows.Scan(
&e.From, &e.To, &e.Kind, &e.FilePath, &e.Line,
&e.Confidence, &e.ConfidenceLabel, &e.Origin, &e.Tier,
&crossRepo, &metaBlob,
&fromRepo, &toRepo,
); err != nil {
continue
}
e.CrossRepo = crossRepo != 0
if len(metaBlob) > 0 {
if m, derr := decodeMeta(metaBlob); derr == nil {
e.Meta = m
}
}
edge := e
out = append(out, graph.CrossRepoCandidateRow{
Edge: &edge,
FromRepo: fromRepo,
ToRepo: toRepo,
})
}
return out
}
// --- ThrowerErrorSurfacer -----------------------------------------------
// ThrowerErrorSurface returns one row per thrower (a node with outgoing
// EdgeThrows edges), aggregating the distinct error targets and the
// distinct literal error-message strings it emits (KindString nodes with
// Meta["context"] == "error_msg", linked by EdgeEmits). pathPrefix gates
// the EdgeThrows rows by their stored FilePath prefix. Throws counts the
// underlying EdgeThrows edges; FilePath / Line seed from the first throws
// edge, falling back to the thrower node's own coordinates when the edge
// carries none — matching the in-memory reference.
func (s *Store) ThrowerErrorSurface(pathPrefix string) []graph.ThrowerErrorRow {
type rowAccum struct {
row graph.ThrowerErrorRow
targetSeen map[string]struct{}
msgSeen map[string]struct{}
}
accums := make(map[string]*rowAccum)
var order []string
// Pass 1: EdgeThrows aggregation (count + distinct targets), keyed by
// thrower. The first edge (by PK insertion order) seeds FilePath /
// Line; an empty edge file/line falls back to the thrower node.
tq := `SELECT from_id, to_id, file_path, line FROM edges WHERE kind = ?`
targs := []any{string(graph.EdgeThrows)}
if pathPrefix != "" {
tq += ` AND file_path LIKE ? ESCAPE '\'`
targs = append(targs, escapeLikePattern(pathPrefix)+"%")
}
tq += ` ORDER BY id`
trows, err := s.db.Query(tq, targs...)
if err != nil {
return nil
}
for trows.Next() {
var from, to, filePath string
var line int
if err := trows.Scan(&from, &to, &filePath, &line); err != nil {
continue
}
acc := accums[from]
if acc == nil {
file := filePath
ln := line
if file == "" || ln == 0 {
if n := s.GetNode(from); n != nil {
if file == "" {
file = n.FilePath
}
if ln == 0 {
ln = n.StartLine
}
}
}
acc = &rowAccum{
row: graph.ThrowerErrorRow{
ThrowerID: from,
FilePath: file,
Line: ln,
},
targetSeen: make(map[string]struct{}),
msgSeen: make(map[string]struct{}),
}
accums[from] = acc
order = append(order, from)
}
acc.row.Throws++
if _, ok := acc.targetSeen[to]; !ok {
acc.targetSeen[to] = struct{}{}
acc.row.ErrorTargets = append(acc.row.ErrorTargets, to)
}
}
_ = trows.Close()
if len(accums) == 0 {
return nil
}
// Pass 2: attach the literal error messages each thrower emits. Join
// each thrower's EdgeEmits out-edges to KindString targets and filter
// Meta["context"] == "error_msg" Go-side (the context lives in the
// JSON Meta blob).
for _, id := range order {
acc := accums[id]
mq := `SELECT n.name, n.meta
FROM edges e
JOIN nodes n ON n.id = e.to_id
WHERE e.from_id = ? AND e.kind = ? AND n.kind = ? AND n.meta IS NOT NULL
ORDER BY e.id`
mrows, err := s.db.Query(mq, id, string(graph.EdgeEmits), string(graph.KindString))
if err != nil {
continue
}
for mrows.Next() {
var name string
var metaBlob []byte
if err := mrows.Scan(&name, &metaBlob); err != nil {
continue
}
meta, derr := decodeMeta(metaBlob)
if derr != nil || meta == nil {
continue
}
ctxLabel, _ := meta["context"].(string)
if ctxLabel != "error_msg" {
continue
}
if _, ok := acc.msgSeen[name]; ok {
continue
}
acc.msgSeen[name] = struct{}{}
acc.row.ErrorMsgs = append(acc.row.ErrorMsgs, name)
}
_ = mrows.Close()
}
out := make([]graph.ThrowerErrorRow, 0, len(order))
for _, id := range order {
out = append(out, accums[id].row)
}
return out
}