query-for-ptag-graph.go raw
1 //go:build !(js && wasm)
2
3 package database
4
5 import (
6 "bytes"
7
8 "next.orly.dev/pkg/lol/chk"
9 "next.orly.dev/pkg/lol/log"
10 "next.orly.dev/pkg/database/indexes"
11 "next.orly.dev/pkg/database/indexes/types"
12 "next.orly.dev/pkg/nostr/encoders/filter"
13 "next.orly.dev/pkg/nostr/encoders/hex"
14 )
15
16 // CanUsePTagGraph determines if a filter can benefit from p-tag graph optimization.
17 //
18 // Requirements:
19 // - Filter must have #p tags
20 // - Filter should NOT have authors (different index is better for that case)
21 // - Optimization works best with kinds filter but is optional
22 func CanUsePTagGraph(f *filter.F) bool {
23 // Must have tags
24 if f.Tags == nil || f.Tags.Len() == 0 {
25 return false
26 }
27
28 // Check if there are any p-tags
29 hasPTags := false
30 for _, t := range *f.Tags {
31 keyBytes := t.Key()
32 if (len(keyBytes) == 1 && keyBytes[0] == 'p') ||
33 (len(keyBytes) == 2 && keyBytes[0] == '#' && keyBytes[1] == 'p') {
34 hasPTags = true
35 break
36 }
37 }
38 if !hasPTags {
39 return false
40 }
41
42 // Don't use graph if there's an authors filter
43 // (TagPubkey index handles that case better)
44 if f.Authors != nil && f.Authors.Len() > 0 {
45 return false
46 }
47
48 return true
49 }
50
51 // QueryPTagGraph uses the pubkey graph index for efficient p-tag queries.
52 //
53 // This query path is optimized for filters like:
54 // {"#p": ["<pubkey>"], "kinds": [1, 6, 7]}
55 //
56 // Performance benefits:
57 // - 41% smaller index keys (16 bytes vs 27 bytes)
58 // - No hash collisions (exact serial match)
59 // - Kind-indexed in key structure
60 // - Direction-aware filtering
61 func (d *D) QueryPTagGraph(f *filter.F) (sers types.Uint40s, err error) {
62 // Extract p-tags from filter
63 var pTags [][]byte
64 for _, t := range *f.Tags {
65 keyBytes := t.Key()
66 if (len(keyBytes) == 1 && keyBytes[0] == 'p') ||
67 (len(keyBytes) == 2 && keyBytes[0] == '#' && keyBytes[1] == 'p') {
68 // Get all values for this p-tag
69 for _, valueBytes := range t.T[1:] {
70 pTags = append(pTags, valueBytes)
71 }
72 }
73 }
74
75 if len(pTags) == 0 {
76 return nil, nil
77 }
78
79 // Resolve pubkey hex → serials
80 var pubkeySerials []*types.Uint40
81 for _, pTagBytes := range pTags {
82 var pubkeyBytes []byte
83
84 // Handle both binary-encoded (33 bytes) and hex-encoded (64 chars) values
85 // Filter tags may come as either format depending on how they were parsed
86 if IsBinaryEncoded(pTagBytes) {
87 // Already binary-encoded, extract the 32-byte hash
88 pubkeyBytes = pTagBytes[:HashLen]
89 } else {
90 // Try to decode as hex using NormalizeTagToHex for consistent handling
91 hexBytes := NormalizeTagToHex(pTagBytes)
92 var decErr error
93 if pubkeyBytes, decErr = hex.Dec(string(hexBytes)); chk.E(decErr) {
94 log.D.F("QueryPTagGraph: failed to decode pubkey hex: %v", decErr)
95 continue
96 }
97 }
98 if len(pubkeyBytes) != 32 {
99 log.D.F("QueryPTagGraph: invalid pubkey length: %d", len(pubkeyBytes))
100 continue
101 }
102
103 // Get serial for this pubkey
104 var serial *types.Uint40
105 if serial, err = d.GetPubkeySerial(pubkeyBytes); chk.E(err) {
106 log.D.F("QueryPTagGraph: pubkey not found in database: %s", hex.Enc(pubkeyBytes))
107 err = nil // Reset error - this just means no events reference this pubkey
108 continue
109 }
110
111 pubkeySerials = append(pubkeySerials, serial)
112 }
113
114 if len(pubkeySerials) == 0 {
115 // None of the pubkeys have serials = no events reference them
116 return nil, nil
117 }
118
119 // Build index ranges for each pubkey serial
120 var ranges []Range
121
122 // Get kinds from filter (if present)
123 var kinds []uint16
124 if f.Kinds != nil && f.Kinds.Len() > 0 {
125 kinds = f.Kinds.ToUint16()
126 }
127
128 // For each pubkey serial, create a range
129 for _, pkSerial := range pubkeySerials {
130 if len(kinds) > 0 {
131 // With kinds: peg|pubkey_serial|kind|direction|event_serial
132 for _, k := range kinds {
133 kind := new(types.Uint16)
134 kind.Set(k)
135 direction := new(types.Letter)
136 direction.Set(types.EdgeDirectionPTagIn) // Direction 2: inbound p-tags
137
138 start := new(bytes.Buffer)
139 idx := indexes.PubkeyEventGraphEnc(pkSerial, kind, direction, nil)
140 if err = idx.MarshalWrite(start); chk.E(err) {
141 return
142 }
143
144 // End range: same prefix with all 0xFF for event serial
145 end := start.Bytes()
146 endWithSerial := make([]byte, len(end)+5)
147 copy(endWithSerial, end)
148 for i := 0; i < 5; i++ {
149 endWithSerial[len(end)+i] = 0xFF
150 }
151
152 ranges = append(ranges, Range{
153 Start: start.Bytes(),
154 End: endWithSerial,
155 })
156 }
157 } else {
158 // Without kinds: we need to scan all kinds for this pubkey
159 // Key structure: peg|pubkey_serial(5)|kind(2)|direction(1)|event_serial(5)
160 // Since direction comes after kind, we can't easily prefix-scan for a specific direction
161 // across all kinds. Instead, we'll iterate through common kinds.
162 //
163 // Common Nostr kinds that use p-tags:
164 // 1 (text note), 6 (repost), 7 (reaction), 9735 (zap), 10002 (relay list)
165 commonKinds := []uint16{1, 6, 7, 9735, 10002, 3, 4, 5, 30023}
166
167 for _, k := range commonKinds {
168 kind := new(types.Uint16)
169 kind.Set(k)
170 direction := new(types.Letter)
171 direction.Set(types.EdgeDirectionPTagIn) // Direction 2: inbound p-tags
172
173 start := new(bytes.Buffer)
174 idx := indexes.PubkeyEventGraphEnc(pkSerial, kind, direction, nil)
175 if err = idx.MarshalWrite(start); chk.E(err) {
176 return
177 }
178
179 // End range: same prefix with all 0xFF for event serial
180 end := start.Bytes()
181 endWithSerial := make([]byte, len(end)+5)
182 copy(endWithSerial, end)
183 for i := 0; i < 5; i++ {
184 endWithSerial[len(end)+i] = 0xFF
185 }
186
187 ranges = append(ranges, Range{
188 Start: start.Bytes(),
189 End: endWithSerial,
190 })
191 }
192 }
193 }
194
195 // Execute scans for each range
196 sers = make(types.Uint40s, 0, len(ranges)*100)
197 for _, rng := range ranges {
198 var rangeSers types.Uint40s
199 if rangeSers, err = d.GetSerialsByRange(rng); chk.E(err) {
200 continue
201 }
202 sers = append(sers, rangeSers...)
203 }
204
205 log.D.F("QueryPTagGraph: found %d events for %d pubkeys", len(sers), len(pubkeySerials))
206 return
207 }
208