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

235 lines
5.8 KiB
Go

package exporter
import (
"fmt"
"io"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// WriteCypher emits the graph as a Cypher script suitable for `cypher-shell`,
// Memgraph Lab, or Neo4j Browser. Every node carries the secondary label
// `GortexNode` so a client can `MATCH (n:GortexNode) DETACH DELETE n` to
// reset before re-loading.
//
// The output is intentionally minimal — pure statements, one per line, no
// comments and no DDL. Comments and `CREATE INDEX … IF NOT EXISTS` syntax
// are not portable between Neo4j 5.x and Memgraph's `.cypherl` loader, so
// we keep the file lowest-common-denominator. Run-instructions and a hint
// for adding the recommended index are emitted on stderr by the CLI, not
// baked into the file.
//
// The MATCH+CREATE per edge is deliberately simple. For graphs > ~50K edges
// loading is slow without an index; create one before loading the edges:
//
// CREATE INDEX ON :GortexNode(id); // Memgraph
// CREATE INDEX FOR (n:GortexNode) ON (n.id); // Neo4j 5.x
func WriteCypher(w io.Writer, g graph.Store, opts Options) (Stats, error) {
cw := &countingWriter{w: w}
nodes, edges, _ := snapshot(g, opts)
stats := Stats{}
for _, n := range nodes {
if err := writeCypherNode(cw, n); err != nil {
return stats, err
}
stats.NodesWritten++
}
for _, e := range edges {
if err := writeCypherEdge(cw, e); err != nil {
return stats, err
}
stats.EdgesWritten++
}
stats.BytesWritten = cw.n
return stats, nil
}
// writeCypherNode emits one CREATE statement per node:
//
// CREATE (:Function:GortexNode {id: "...", name: "...", ...});
func writeCypherNode(w io.Writer, n *graph.Node) error {
label := nodeLabel(n.Kind)
props := []propPair{
{"id", n.ID},
{"name", n.Name},
}
if n.QualName != "" {
props = append(props, propPair{"qual_name", n.QualName})
}
if n.FilePath != "" {
props = append(props, propPair{"file_path", n.FilePath})
}
if n.StartLine > 0 {
props = append(props, propPair{"start_line", int64(n.StartLine)})
}
if n.EndLine > 0 {
props = append(props, propPair{"end_line", int64(n.EndLine)})
}
if n.Language != "" {
props = append(props, propPair{"language", n.Language})
}
if n.RepoPrefix != "" {
props = append(props, propPair{"repo_prefix", n.RepoPrefix})
}
if n.WorkspaceID != "" {
props = append(props, propPair{"workspace_id", n.WorkspaceID})
}
if n.ProjectID != "" {
props = append(props, propPair{"project_id", n.ProjectID})
}
for _, e := range flattenMeta(n.Meta) {
// Don't shadow our top-level keys.
if isReservedNodeKey(e.Key) {
continue
}
props = append(props, propPair(e))
}
_, err := fmt.Fprintf(w, "CREATE (:%s:GortexNode %s);\n", label, cypherProps(props))
return err
}
// writeCypherEdge emits one MATCH + CREATE per edge.
func writeCypherEdge(w io.Writer, e *graph.Edge) error {
relType := edgeRelType(e.Kind)
props := []propPair{}
if e.Confidence > 0 {
props = append(props, propPair{"confidence", e.Confidence})
}
if e.ConfidenceLabel != "" {
props = append(props, propPair{"confidence_label", e.ConfidenceLabel})
}
if e.Origin != "" {
props = append(props, propPair{"origin", e.Origin})
}
if e.FilePath != "" {
props = append(props, propPair{"file_path", e.FilePath})
}
if e.Line > 0 {
props = append(props, propPair{"line", int64(e.Line)})
}
if e.CrossRepo {
props = append(props, propPair{"cross_repo", true})
}
for _, m := range flattenMeta(e.Meta) {
if isReservedEdgeKey(m.Key) {
continue
}
props = append(props, propPair(m))
}
_, err := fmt.Fprintf(w,
"MATCH (a:GortexNode {id: %s}), (b:GortexNode {id: %s}) CREATE (a)-[:%s %s]->(b);\n",
cypherString(e.From), cypherString(e.To), relType, cypherProps(props),
)
return err
}
// propPair is one (key, value) row inside a Cypher property map.
type propPair struct {
Key string
Value any
}
// cypherProps formats a slice of pairs as a Cypher map literal: `{k: v, k: v}`.
// Empty slice → `{}` (always emit braces; Cypher's grammar accepts an empty map).
func cypherProps(pairs []propPair) string {
if len(pairs) == 0 {
return "{}"
}
var b strings.Builder
b.WriteByte('{')
for i, p := range pairs {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(p.Key)
b.WriteString(": ")
b.WriteString(cypherValue(p.Value))
}
b.WriteByte('}')
return b.String()
}
// cypherValue formats a single value as a Cypher literal.
func cypherValue(v any) string {
switch x := v.(type) {
case nil:
return "null"
case string:
return cypherString(x)
case bool:
if x {
return "true"
}
return "false"
case int:
return fmt.Sprintf("%d", x)
case int32:
return fmt.Sprintf("%d", x)
case int64:
return fmt.Sprintf("%d", x)
case uint:
return fmt.Sprintf("%d", x)
case uint32:
return fmt.Sprintf("%d", x)
case uint64:
return fmt.Sprintf("%d", x)
case float32:
return fmt.Sprintf("%g", x)
case float64:
return fmt.Sprintf("%g", x)
default:
return cypherString(fmt.Sprintf("%v", x))
}
}
// cypherString returns a single-quoted Cypher string with the standard escape
// set: backslash, single quote, newline, carriage return, tab.
func cypherString(s string) string {
var b strings.Builder
b.Grow(len(s) + 2)
b.WriteByte('\'')
for _, r := range s {
switch r {
case '\\':
b.WriteString(`\\`)
case '\'':
b.WriteString(`\'`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
default:
b.WriteRune(r)
}
}
b.WriteByte('\'')
return b.String()
}
func isReservedNodeKey(k string) bool {
switch k {
case "id", "name", "qual_name", "file_path", "start_line", "end_line",
"language", "repo_prefix", "workspace_id", "project_id":
return true
}
return false
}
func isReservedEdgeKey(k string) bool {
switch k {
case "confidence", "confidence_label", "origin", "file_path", "line", "cross_repo":
return true
}
return false
}