tinyrelayfile
tinyrelay/internal/storage/query.go/

internal/storage/query.go raw
1package storage
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "sort"
8 "strings"
9 "time"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/event"
12)
13
14// Access controls which private and callback events a query may return. All
15// bypasses recipient filtering but still excludes hidden, pending and expired
16// rows.
17type Access struct {
18 PubKeys []string
19 All bool
20}
21
22// QueryOptions supplies query time, access principals, an optional result
23// limit and a cursor from a prior Query result.
24type QueryOptions struct {
25 Now int64
26 Access Access
27 Limit int
28 Before *EventCursor
29}
30
31// EventCursor follows the stable newest-first query order.
32type EventCursor struct {
33 CreatedAt int64
34 ID string
35}
36
37// QueryResult contains the matching events and whether the limit hid more
38// rows.
39type QueryResult struct {
40 Events []event.Event
41 More bool
42}
43
44type predicate struct {
45 conditions []string
46 args []any
47}
48
49func (p *predicate) add(condition string, args ...any) {
50 p.conditions = append(p.conditions, condition)
51 p.args = append(p.args, args...)
52}
53
54func (p *predicate) list(column string, value any) error {
55 b, err := json.Marshal(value)
56 if err != nil {
57 return fmt.Errorf("encode filter list: %w", err)
58 }
59 p.add(column+" IN (SELECT value FROM json_each(?))", string(b))
60 return nil
61}
62
63func where(f event.Filter, opts QueryOptions) (predicate, error) {
64 var p predicate
65 if opts.Before != nil {
66 p.add("(created_at<? OR (created_at=? AND id>?))", opts.Before.CreatedAt, opts.Before.CreatedAt, opts.Before.ID)
67 }
68 if f.IDs != nil {
69 if err := p.list("id", f.IDs); err != nil {
70 return p, err
71 }
72 }
73 if f.Authors != nil {
74 if err := p.list("pubkey", f.Authors); err != nil {
75 return p, err
76 }
77 }
78 if f.Kinds != nil {
79 if err := p.list("kind", f.Kinds); err != nil {
80 return p, err
81 }
82 }
83 if f.Since != nil {
84 p.add("created_at>=?", *f.Since)
85 }
86 if f.Until != nil {
87 p.add("created_at<=?", *f.Until)
88 }
89 keys := make([]string, 0, len(f.Tags))
90 for name := range f.Tags {
91 keys = append(keys, name)
92 }
93 sort.Strings(keys)
94 for _, name := range keys {
95 values, err := json.Marshal(f.Tags[name])
96 if err != nil {
97 return p, fmt.Errorf("encode tag filter: %w", err)
98 }
99 p.add("EXISTS(SELECT 1 FROM tags WHERE tags.event_id=events.id AND name=? AND value IN(SELECT value FROM json_each(?)))", name, string(values))
100 }
101 terms := event.SearchTerms(f.Search)
102 if len(terms) > 0 {
103 quoted := make([]string, len(terms))
104 for i, term := range terms {
105 quoted[i] = `"` + strings.ReplaceAll(term, `"`, `""`) + `"`
106 }
107 p.add("seq IN (SELECT rowid FROM search WHERE search MATCH ?)", strings.Join(quoted, " AND "))
108 }
109 p.add("(expires=0 OR expires>?)", opts.Now)
110 p.add("NOT EXISTS(SELECT 1 FROM hidden_events WHERE hidden_events.id=events.id)")
111 p.add("NOT EXISTS(SELECT 1 FROM pending_events WHERE pending_events.id=events.id)")
112 if err := addAccess(&p, opts.Access); err != nil {
113 return p, err
114 }
115 return p, nil
116}
117
118func addAccess(p *predicate, who Access) error {
119 keys, err := json.Marshal(who.PubKeys)
120 if err != nil {
121 return fmt.Errorf("encode read principals: %w", err)
122 }
123 // Callback registrations never become visible through internal all-access.
124 p.add("(kind<>"+fmt.Sprint(event.KIND_PUSH_REGISTRATION)+" OR pubkey IN(SELECT value FROM json_each(?)))", string(keys))
125 if who.All {
126 return nil
127 }
128 // NIP-59 seals (kind 13) carry no recipient tag; only their author may
129 // retrieve them. Gift wraps and other private events are recipient scoped.
130 p.add("(kind NOT IN(4,13,1059,21059,24133) OR pubkey IN(SELECT value FROM json_each(?)) OR (kind<>13 AND EXISTS(SELECT 1 FROM tags WHERE event_id=events.id AND name='p' AND value IN(SELECT value FROM json_each(?)))))", string(keys), string(keys))
131 return nil
132}
133
134func (s *Store) Query(ctx context.Context, f event.Filter, opts QueryOptions) (result QueryResult, err error) {
135 started := time.Now()
136 defer func() { s.observe("query", started, err) }()
137 p, err := where(f, opts)
138 if err != nil {
139 return result, err
140 }
141 query := "SELECT raw FROM events WHERE " + strings.Join(p.conditions, " AND ") + " ORDER BY created_at DESC,id ASC"
142 limit := opts.Limit
143 if f.Limit != nil && (limit <= 0 || *f.Limit < limit) {
144 limit = *f.Limit
145 }
146 limited := opts.Limit > 0 || f.Limit != nil
147 if limited {
148 if limit < 0 {
149 limit = 0
150 }
151 query += " LIMIT ?"
152 p.args = append(p.args, int64(limit)+1)
153 }
154 rows, err := s.db.QueryContext(ctx, query, p.args...)
155 if err != nil {
156 return result, fmt.Errorf("query events: %w", err)
157 }
158 defer func() {
159 if closeErr := rows.Close(); err == nil {
160 err = closeErr
161 }
162 }()
163 result.Events = []event.Event{}
164 for rows.Next() {
165 if limited && len(result.Events) == limit {
166 result.More = true
167 break
168 }
169 var raw string
170 if err := rows.Scan(&raw); err != nil {
171 return result, fmt.Errorf("read event: %w", err)
172 }
173 var e event.Event
174 if err := json.Unmarshal([]byte(raw), &e); err != nil {
175 return result, fmt.Errorf("decode stored event: %w", err)
176 }
177 result.Events = append(result.Events, e)
178 }
179 return result, rows.Err()
180}
181
182func (s *Store) Count(ctx context.Context, filters []event.Filter, opts QueryOptions) (count int64, err error) {
183 started := time.Now()
184 defer func() { s.observe("count", started, err) }()
185 if len(filters) == 0 {
186 return 0, nil
187 }
188 var ors []string
189 var args []any
190 for _, f := range filters {
191 p, err := where(f, opts)
192 if err != nil {
193 return 0, err
194 }
195 ors = append(ors, "("+strings.Join(p.conditions, " AND ")+")")
196 args = append(args, p.args...)
197 }
198 err = s.db.QueryRowContext(ctx, "SELECT count(*) FROM events WHERE "+strings.Join(ors, " OR "), args...).Scan(&count)
199 if err != nil {
200 return 0, fmt.Errorf("count events: %w", err)
201 }
202 return count, nil
203}
204
205type CursorEvent struct {
206 Sequence int64 `json:"seq"`
207 Event event.Event `json:"event"`
208}
209
210func (s *Store) After(ctx context.Context, seq int64, f event.Filter, opts QueryOptions) ([]CursorEvent, error) {
211 p, err := where(f, opts)
212 if err != nil {
213 return nil, err
214 }
215 p.add("seq>?", seq)
216 q := "SELECT seq,raw FROM events WHERE " + strings.Join(p.conditions, " AND ") + " ORDER BY seq"
217 if opts.Limit > 0 {
218 q += " LIMIT ?"
219 p.args = append(p.args, opts.Limit)
220 }
221 rows, err := s.db.QueryContext(ctx, q, p.args...)
222 if err != nil {
223 return nil, fmt.Errorf("query event cursor: %w", err)
224 }
225 defer rows.Close()
226 var result []CursorEvent
227 for rows.Next() {
228 var item CursorEvent
229 var raw string
230 if err := rows.Scan(&item.Sequence, &raw); err != nil {
231 return nil, err
232 }
233 if err := json.Unmarshal([]byte(raw), &item.Event); err != nil {
234 return nil, err
235 }
236 result = append(result, item)
237 }
238 return result, rows.Err()
239}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs