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
205 lines
7.4 KiB
Go
205 lines
7.4 KiB
Go
package store_sqlite
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// These methods were added to graph.Store after the sqlite backend was
|
|
// first removed; they are restored here so *Store satisfies the current
|
|
// interface. All reuse the chunked IN-list / raw-SQL helpers in store.go
|
|
// (queryNodesSQL / queryEdgesSQL / lookupChunkSize / minInt). SQLite's
|
|
// planner drives every one through the existing secondary indexes.
|
|
|
|
// lookupNodeCols is the canonical node column list (and scan order) for
|
|
// every node-shaped SELECT in the package. It must stay in sync with
|
|
// scanNode. The struct columns (start_column/end_column) sit with the line
|
|
// range; the promoted meta columns (signature/visibility/doc/external/
|
|
// return_type/is_async/is_static/is_abstract/is_exported/updated_at/
|
|
// data_class/semantic_type/semantic_source) precede meta.
|
|
const lookupNodeCols = `id, kind, name, qual_name, file_path, start_line, end_line, start_column, end_column, language, repo_prefix, workspace_id, project_id, signature, visibility, doc, external, return_type, is_async, is_static, is_abstract, is_exported, updated_at, data_class, semantic_type, semantic_source, meta`
|
|
|
|
// lookupNodeColsLight is lookupNodeCols without the trailing meta column —
|
|
// the projection GetRepoNodesLight uses so a repo-scoped scan never
|
|
// transfers or decodes a single blob. Derived, not hand-duplicated, so it
|
|
// can never drift out of sync with lookupNodeCols / scanNode.
|
|
var lookupNodeColsLight = strings.TrimSuffix(lookupNodeCols, ", meta")
|
|
|
|
const lookupEdgeCols = `from_id, to_id, kind, file_path, line, confidence, confidence_label, origin, tier, cross_repo, meta, resolve_terminal, resolve_terminal_reason`
|
|
|
|
// Compile-time assertion: *Store satisfies graph.NodeNameClassCounter.
|
|
var _ graph.NodeNameClassCounter = (*Store)(nil)
|
|
|
|
// CountNodesByNameClass implements graph.NodeNameClassCounter: for each
|
|
// distinct name, it tallies how many nodes.name matches are Real (is_stub =
|
|
// 0 and kind IN definitionKinds) vs Stub (is_stub = 1), server-side via
|
|
// nodes_by_name — one aggregate query per chunk instead of one
|
|
// FindNodesByName round trip per name. A name absent from the returned map
|
|
// has no matching node at all (Real == Stub == 0 either way).
|
|
func (s *Store) CountNodesByNameClass(names []string, definitionKinds []graph.NodeKind) map[string]graph.NodeNameClassCount {
|
|
_, kindArgs := aggDedupeNodeKinds(definitionKinds)
|
|
if len(kindArgs) == 0 {
|
|
return nil
|
|
}
|
|
uniq := dedupeNonEmpty(names)
|
|
if len(uniq) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]graph.NodeNameClassCount, len(uniq))
|
|
kindPlaceholders := inPlaceholders(len(kindArgs))
|
|
for i := 0; i < len(uniq); i += lookupChunkSize {
|
|
end := minInt(i+lookupChunkSize, len(uniq))
|
|
chunk := uniq[i:end]
|
|
q := `SELECT name,
|
|
SUM(CASE WHEN is_stub = 0 AND kind IN (` + kindPlaceholders + `) THEN 1 ELSE 0 END),
|
|
SUM(CASE WHEN is_stub = 1 THEN 1 ELSE 0 END)
|
|
FROM nodes
|
|
WHERE name IN (` + inPlaceholders(len(chunk)) + `)
|
|
GROUP BY name`
|
|
args := make([]any, 0, len(kindArgs)+len(chunk))
|
|
args = append(args, kindArgs...)
|
|
args = append(args, toAnyArgs(chunk)...)
|
|
rows, err := s.db.Query(q, args...)
|
|
if err != nil {
|
|
panicOnFatal(err)
|
|
return out
|
|
}
|
|
for rows.Next() {
|
|
var name string
|
|
var c graph.NodeNameClassCount
|
|
if err := rows.Scan(&name, &c.Real, &c.Stub); err != nil {
|
|
_ = rows.Close()
|
|
panicOnFatal(err)
|
|
return out
|
|
}
|
|
out[name] = c
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
_ = rows.Close()
|
|
panicOnFatal(err)
|
|
return out
|
|
}
|
|
_ = rows.Close()
|
|
}
|
|
return out
|
|
}
|
|
|
|
// FindNodesByNameContaining returns nodes whose Name contains substr,
|
|
// case-insensitively (SQLite's LIKE is ASCII case-insensitive). An empty
|
|
// substring matches nothing (parity with the in-memory store); a limit > 0
|
|
// caps the result set. The leading-wildcard LIKE is a deliberate full scan —
|
|
// no index accelerates an unanchored substring — matching the in-memory
|
|
// strings.Contains fallback. % and _ in substr are escaped so they match
|
|
// literally.
|
|
func (s *Store) FindNodesByNameContaining(substr string, limit int) []*graph.Node {
|
|
if substr == "" {
|
|
return nil
|
|
}
|
|
pattern := "%" + escapeLikePattern(substr) + "%"
|
|
q := `SELECT ` + lookupNodeCols + ` FROM nodes WHERE name LIKE ? ESCAPE '\' ORDER BY id`
|
|
if limit > 0 {
|
|
return s.queryNodesSQL(q+` LIMIT ?`, pattern, limit)
|
|
}
|
|
return s.queryNodesSQL(q, pattern)
|
|
}
|
|
|
|
// GetNodesByQualNames returns a map qualName→*Node (first match per
|
|
// qual_name) for the batch — the qual-name twin of FindNodesByNames, used to
|
|
// pre-warm import resolution. Driven by the unique nodes_by_qual index.
|
|
func (s *Store) GetNodesByQualNames(qualNames []string) map[string]*graph.Node {
|
|
uniq := dedupeNonEmpty(qualNames)
|
|
if len(uniq) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]*graph.Node, len(uniq))
|
|
for i := 0; i < len(uniq); i += lookupChunkSize {
|
|
end := minInt(i+lookupChunkSize, len(uniq))
|
|
chunk := uniq[i:end]
|
|
q := `SELECT ` + lookupNodeCols + ` FROM nodes WHERE qual_name IN (` + inPlaceholders(len(chunk)) + `)`
|
|
for _, n := range s.queryNodesSQL(q, toAnyArgs(chunk)...) {
|
|
if n == nil {
|
|
continue
|
|
}
|
|
if _, ok := out[n.QualName]; !ok {
|
|
out[n.QualName] = n
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GetOutEdgesByNodeIDs batches per-node out-edge fan-out into one query per
|
|
// chunk. Missing IDs are simply absent from the returned map.
|
|
func (s *Store) GetOutEdgesByNodeIDs(ids []string) map[string][]*graph.Edge {
|
|
return s.edgesByNodeIDs(ids, "from_id", func(e *graph.Edge) string { return e.From })
|
|
}
|
|
|
|
// GetInEdgesByNodeIDs is the incoming-edge twin of GetOutEdgesByNodeIDs.
|
|
func (s *Store) GetInEdgesByNodeIDs(ids []string) map[string][]*graph.Edge {
|
|
return s.edgesByNodeIDs(ids, "to_id", func(e *graph.Edge) string { return e.To })
|
|
}
|
|
|
|
// edgesByNodeIDs runs the chunked IN-list edge fetch keyed on the given
|
|
// column (from_id or to_id), grouping results by the supplied key extractor.
|
|
func (s *Store) edgesByNodeIDs(ids []string, col string, key func(*graph.Edge) string) map[string][]*graph.Edge {
|
|
uniq := dedupeNonEmpty(ids)
|
|
if len(uniq) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string][]*graph.Edge, len(uniq))
|
|
for i := 0; i < len(uniq); i += lookupChunkSize {
|
|
end := minInt(i+lookupChunkSize, len(uniq))
|
|
chunk := uniq[i:end]
|
|
q := `SELECT ` + lookupEdgeCols + ` FROM edges WHERE ` + col + ` IN (` + inPlaceholders(len(chunk)) + `)`
|
|
for _, e := range s.queryEdgesSQL(q, toAnyArgs(chunk)...) {
|
|
if e == nil {
|
|
continue
|
|
}
|
|
k := key(e)
|
|
out[k] = append(out[k], e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// dedupeNonEmpty drops empties and duplicates, preserving first-seen order.
|
|
func dedupeNonEmpty(in []string) []string {
|
|
seen := make(map[string]struct{}, len(in))
|
|
out := make([]string, 0, len(in))
|
|
for _, v := range in {
|
|
if v == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[v]; ok {
|
|
continue
|
|
}
|
|
seen[v] = struct{}{}
|
|
out = append(out, v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// inPlaceholders returns "?,?,?" for n bound parameters.
|
|
func inPlaceholders(n int) string {
|
|
if n <= 0 {
|
|
return ""
|
|
}
|
|
return strings.Repeat(",?", n)[1:]
|
|
}
|
|
|
|
// toAnyArgs widens a string slice for variadic Query/Exec args.
|
|
func toAnyArgs(ss []string) []any {
|
|
args := make([]any, len(ss))
|
|
for i, v := range ss {
|
|
args[i] = v
|
|
}
|
|
return args
|
|
}
|
|
|
|
// escapeLikePattern escapes the LIKE metacharacters so the substring matches
|
|
// literally under `... LIKE ? ESCAPE '\'`.
|
|
func escapeLikePattern(s string) string {
|
|
return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(s)
|
|
}
|