-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcypher.go
More file actions
132 lines (125 loc) · 3.92 KB
/
cypher.go
File metadata and controls
132 lines (125 loc) · 3.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package graph
import (
"fmt"
"time"
kuzu "github.com/kuzudb/go-kuzu"
)
// DefaultQueryTimeout matches the Java side's DBMS-level cap
// (GraphDatabaseSettings.transaction_timeout = 30s in Neo4jConfig).
// Kuzu accepts the timeout in milliseconds on the Connection.
const DefaultQueryTimeout = 30 * time.Second
// Cypher runs a Cypher statement and returns rows as []map[string]any. For
// DDL or void queries the returned slice may be empty (or contain whatever
// status row Kuzu emits). If args is supplied the query is prepared and
// bound; otherwise it is executed directly.
//
// The caller-supplied map is read-only — parameter values are copied through
// go-kuzu's Execute path.
func (s *Store) Cypher(query string, args ...map[string]any) ([]map[string]any, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.conn == nil {
return nil, fmt.Errorf("graph: store closed")
}
if s.readOnly {
if kw := MutationKeyword(query); kw != "" {
return nil, fmt.Errorf("graph: write query rejected on read-only store (blocked keyword: %s)", kw)
}
}
var params map[string]any
if len(args) > 0 {
params = args[0]
}
qr, err := execQuery(s.conn, query, params)
if err != nil {
return nil, fmt.Errorf("graph: cypher: %w", err)
}
defer qr.Close()
return decodeResult(qr)
}
// CypherRows runs query, materialises up to maxRows result rows, and
// reports whether the query produced more rows than the cap. Used by
// the run_cypher MCP tool which needs to surface a `truncated` flag
// without inlining `LIMIT N` into the user-supplied query string (the
// query may already have its own LIMIT — see the McpTools row-cap
// gotcha in CLAUDE.md).
//
// The mutation gate from Cypher() applies here too: on a read-only
// store, any blocked-keyword query short-circuits with an error.
func (s *Store) CypherRows(query string, args map[string]any, maxRows int) ([]map[string]any, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.conn == nil {
return nil, false, fmt.Errorf("graph: store closed")
}
if s.readOnly {
if kw := MutationKeyword(query); kw != "" {
return nil, false, fmt.Errorf("graph: write query rejected on read-only store (blocked keyword: %s)", kw)
}
}
qr, err := execQuery(s.conn, query, args)
if err != nil {
return nil, false, fmt.Errorf("graph: cypher: %w", err)
}
defer qr.Close()
if maxRows <= 0 {
maxRows = 1
}
rows := make([]map[string]any, 0, maxRows)
truncated := false
for qr.HasNext() {
if len(rows) >= maxRows {
// Drain one more tuple to confirm there *are* more rows; we don't
// keep the value, just the truncated flag.
truncated = true
t, err := qr.Next()
if err == nil {
t.Close()
}
break
}
tuple, err := qr.Next()
if err != nil {
return rows, truncated, fmt.Errorf("next: %w", err)
}
row, err := tuple.GetAsMap()
tuple.Close()
if err != nil {
return rows, truncated, fmt.Errorf("decode row: %w", err)
}
rows = append(rows, row)
}
return rows, truncated, nil
}
// execQuery dispatches to Query for no-params and Prepare+Execute for
// parameterized queries.
func execQuery(conn *kuzu.Connection, query string, params map[string]any) (*kuzu.QueryResult, error) {
if params == nil {
return conn.Query(query)
}
stmt, err := conn.Prepare(query)
if err != nil {
return nil, fmt.Errorf("prepare: %w", err)
}
defer stmt.Close()
return conn.Execute(stmt, params)
}
// decodeResult walks the FlatTuple cursor and materialises each row as a
// map keyed by the result's column names. Cells are converted to Go values
// via go-kuzu's built-in kuzuValueToGoValue (exposed through FlatTuple.GetAsMap).
func decodeResult(qr *kuzu.QueryResult) ([]map[string]any, error) {
var rows []map[string]any
for qr.HasNext() {
tuple, err := qr.Next()
if err != nil {
return rows, fmt.Errorf("next: %w", err)
}
row, err := tuple.GetAsMap()
tuple.Close()
if err != nil {
return rows, fmt.Errorf("decode row: %w", err)
}
rows = append(rows, row)
}
return rows, nil
}