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
220 lines
6.7 KiB
Go
220 lines
6.7 KiB
Go
package store_sqlite
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// Compile-time assertions that the SQLite Store satisfies the optional
|
|
// reference-facts persistence capability. Persisting resolved-reference facts
|
|
// in the same backend the graph lives in makes a reference's resolution an
|
|
// auditable, diffable record and a warm-restart seed.
|
|
var (
|
|
_ graph.RefFactsWriter = (*Store)(nil)
|
|
_ graph.RefFactsReader = (*Store)(nil)
|
|
)
|
|
|
|
// refFactChunk bounds rows per multi-row INSERT. 11 params/row; 80 rows = 880
|
|
// host params, under SQLite's 999 default. Mirrors shingleChunk.
|
|
const refFactChunk = 80
|
|
|
|
// candidate-list separator (unit separator — never appears in identifiers).
|
|
const refFactCandSep = "\x1f"
|
|
|
|
func encodeCandidates(c []string) string { return strings.Join(c, refFactCandSep) }
|
|
|
|
func decodeCandidates(s string) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(s, refFactCandSep)
|
|
}
|
|
|
|
// BulkSetRefFacts persists resolved-reference facts for one repo prefix in a
|
|
// single transaction, chunked under the host-parameter limit. Idempotent on
|
|
// (repo_prefix, from_id, to_id, kind, line). Empty input is a no-op.
|
|
func (s *Store) BulkSetRefFacts(repoPrefix string, facts []graph.RefFact) error {
|
|
if len(facts) == 0 {
|
|
return nil
|
|
}
|
|
s.writeMu.Lock()
|
|
defer s.writeMu.Unlock()
|
|
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback() //nolint:errcheck // rollback after Commit is a no-op
|
|
|
|
for start := 0; start < len(facts); start += refFactChunk {
|
|
end := start + refFactChunk
|
|
if end > len(facts) {
|
|
end = len(facts)
|
|
}
|
|
batch := facts[start:end]
|
|
args := make([]any, 0, len(batch)*11)
|
|
stmt := make([]byte, 0, 96+len(batch)*24)
|
|
stmt = append(stmt, "INSERT OR REPLACE INTO ref_facts (repo_prefix, from_id, to_id, kind, ref_name, line, origin, tier, candidates, file_path, lang) VALUES "...)
|
|
for i, f := range batch {
|
|
if i > 0 {
|
|
stmt = append(stmt, ',')
|
|
}
|
|
stmt = append(stmt, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"...)
|
|
args = append(args, repoPrefix, f.FromID, f.ToID, f.Kind, f.RefName, f.Line, f.Origin, f.Tier, encodeCandidates(f.Candidates), f.FilePath, f.Lang)
|
|
}
|
|
if _, err := tx.Exec(string(stmt), args...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// DeleteRefFactsByFiles drops all facts sourced in the supplied files for one
|
|
// repo prefix, chunked into `file_path IN (…)` DELETEs. Empty input is a no-op.
|
|
func (s *Store) DeleteRefFactsByFiles(repoPrefix string, files []string) error {
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
s.writeMu.Lock()
|
|
defer s.writeMu.Unlock()
|
|
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback() //nolint:errcheck // rollback after Commit is a no-op
|
|
|
|
for start := 0; start < len(files); start += refFactChunk {
|
|
end := start + refFactChunk
|
|
if end > len(files) {
|
|
end = len(files)
|
|
}
|
|
chunk := files[start:end]
|
|
args := make([]any, 0, len(chunk)+1)
|
|
args = append(args, repoPrefix)
|
|
stmt := make([]byte, 0, 64+len(chunk)*2)
|
|
stmt = append(stmt, "DELETE FROM ref_facts WHERE repo_prefix = ? AND file_path IN ("...)
|
|
for i, f := range chunk {
|
|
if i > 0 {
|
|
stmt = append(stmt, ',')
|
|
}
|
|
stmt = append(stmt, '?')
|
|
args = append(args, f)
|
|
}
|
|
stmt = append(stmt, ')')
|
|
if _, err := tx.Exec(string(stmt), args...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// LoadRefFactsByFiles returns the persisted facts for one repo prefix, scoped
|
|
// to the given files (all files when files is empty). Always non-nil.
|
|
func (s *Store) LoadRefFactsByFiles(repoPrefix string, files []string) ([]graph.RefFact, error) {
|
|
out := []graph.RefFact{}
|
|
scan := func(query string, args ...any) error {
|
|
rows, err := s.db.Query(query, args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var f graph.RefFact
|
|
var cand string
|
|
if err := rows.Scan(&f.FromID, &f.ToID, &f.Kind, &f.RefName, &f.Line, &f.Origin, &f.Tier, &cand, &f.FilePath, &f.Lang); err != nil {
|
|
return err
|
|
}
|
|
f.RepoPrefix = repoPrefix
|
|
f.Candidates = decodeCandidates(cand)
|
|
out = append(out, f)
|
|
}
|
|
return rows.Err()
|
|
}
|
|
const cols = `from_id, to_id, kind, ref_name, line, origin, tier, candidates, file_path, lang`
|
|
if len(files) == 0 {
|
|
if err := scan(`SELECT `+cols+` FROM ref_facts WHERE repo_prefix = ?`, repoPrefix); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
for start := 0; start < len(files); start += refFactChunk {
|
|
end := start + refFactChunk
|
|
if end > len(files) {
|
|
end = len(files)
|
|
}
|
|
chunk := files[start:end]
|
|
args := make([]any, 0, len(chunk)+1)
|
|
args = append(args, repoPrefix)
|
|
stmt := make([]byte, 0, 96+len(chunk)*2)
|
|
stmt = append(stmt, "SELECT "+cols+" FROM ref_facts WHERE repo_prefix = ? AND file_path IN ("...)
|
|
for i, f := range chunk {
|
|
if i > 0 {
|
|
stmt = append(stmt, ',')
|
|
}
|
|
stmt = append(stmt, '?')
|
|
args = append(args, f)
|
|
}
|
|
stmt = append(stmt, ')')
|
|
if err := scan(string(stmt), args...); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// LoadRefFactsByTargets returns the persisted facts that resolve TO any of
|
|
// the given node IDs for one repo prefix, grouped by source file path — the
|
|
// reverse lookup incremental re-resolution uses to find the files that
|
|
// referenced a changed symbol after its live in-edges were evicted. Served by
|
|
// the ref_facts_by_target index, chunked under the host-parameter limit.
|
|
// Always non-nil; empty input is a no-op.
|
|
func (s *Store) LoadRefFactsByTargets(repoPrefix string, targetIDs []string) (map[string][]graph.RefFact, error) {
|
|
out := map[string][]graph.RefFact{}
|
|
if len(targetIDs) == 0 {
|
|
return out, nil
|
|
}
|
|
const cols = `from_id, to_id, kind, ref_name, line, origin, tier, candidates, file_path, lang`
|
|
for start := 0; start < len(targetIDs); start += refFactChunk {
|
|
end := start + refFactChunk
|
|
if end > len(targetIDs) {
|
|
end = len(targetIDs)
|
|
}
|
|
chunk := targetIDs[start:end]
|
|
args := make([]any, 0, len(chunk)+1)
|
|
args = append(args, repoPrefix)
|
|
stmt := make([]byte, 0, 96+len(chunk)*2)
|
|
stmt = append(stmt, "SELECT "+cols+" FROM ref_facts WHERE repo_prefix = ? AND to_id IN ("...)
|
|
for i, id := range chunk {
|
|
if i > 0 {
|
|
stmt = append(stmt, ',')
|
|
}
|
|
stmt = append(stmt, '?')
|
|
args = append(args, id)
|
|
}
|
|
stmt = append(stmt, ')')
|
|
rows, err := s.db.Query(string(stmt), args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var f graph.RefFact
|
|
var cand string
|
|
if err := rows.Scan(&f.FromID, &f.ToID, &f.Kind, &f.RefName, &f.Line, &f.Origin, &f.Tier, &cand, &f.FilePath, &f.Lang); err != nil {
|
|
_ = rows.Close()
|
|
return nil, err
|
|
}
|
|
f.RepoPrefix = repoPrefix
|
|
f.Candidates = decodeCandidates(cand)
|
|
out[f.FilePath] = append(out[f.FilePath], f)
|
|
}
|
|
err = rows.Err()
|
|
_ = rows.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|