chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,500 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user