1 package neo4j
2 3 import (
4 "context"
5 "fmt"
6 )
7 8 // applySchema creates Neo4j constraints and indexes for Nostr events
9 // Neo4j uses Cypher queries to define schema constraints and indexes
10 // Includes both base Nostr relay schema and optional WoT extensions
11 //
12 // Schema categories:
13 // - MANDATORY (NIP-01): Required for basic REQ filter support per NIP-01 spec
14 // - OPTIONAL (Internal): Used for relay internal operations, not required by NIP-01
15 // - OPTIONAL (WoT): Web of Trust extensions, relay-specific functionality
16 //
17 // NIP-01 REQ filter fields that require indexing:
18 // - ids: array of event IDs -> Event.id (MANDATORY)
19 // - authors: array of pubkeys -> Author.pubkey (MANDATORY)
20 // - kinds: array of integers -> Event.kind (MANDATORY)
21 // - #<tag>: tag queries like #e, #p -> Tag.type + Tag.value (MANDATORY)
22 // - since: unix timestamp -> Event.created_at (MANDATORY)
23 // - until: unix timestamp -> Event.created_at (MANDATORY)
24 // - limit: integer -> no index needed, just result limiting
25 func (n *N) applySchema(ctx context.Context) error {
26 n.Logger.Infof("applying Nostr schema to neo4j")
27 28 // Create constraints and indexes using Cypher queries
29 // Constraints ensure uniqueness and are automatically indexed
30 constraints := []string{
31 // ============================================================
32 // === MANDATORY: NIP-01 REQ Query Support ===
33 // These constraints are required for basic Nostr relay operation
34 // ============================================================
35 36 // MANDATORY (NIP-01): Event.id uniqueness for "ids" filter
37 // REQ filters can specify: {"ids": ["<event_id>", ...]}
38 "CREATE CONSTRAINT event_id_unique IF NOT EXISTS FOR (e:Event) REQUIRE e.id IS UNIQUE",
39 40 // MANDATORY (NIP-01): NostrUser.pubkey uniqueness for "authors" filter
41 // REQ filters can specify: {"authors": ["<pubkey>", ...]}
42 // Events are linked to NostrUser nodes via AUTHORED_BY relationship
43 // NOTE: NostrUser unifies both NIP-01 author tracking and WoT social graph
44 "CREATE CONSTRAINT nostrUser_pubkey IF NOT EXISTS FOR (n:NostrUser) REQUIRE n.pubkey IS UNIQUE",
45 46 // ============================================================
47 // === OPTIONAL: Addressable Event Support (NIP-33) ===
48 // These support parameterized replaceable events (kinds 30000-39999)
49 // ============================================================
50 51 // OPTIONAL (NIP-33): naddr uniqueness for addressable events
52 // Format: pubkey:kind:dtag (colon-delimited coordinate)
53 // Ensures only one event per author+kind+d-tag combination
54 "CREATE CONSTRAINT naddr_unique IF NOT EXISTS FOR (e:Event) REQUIRE e.naddr IS UNIQUE",
55 56 // ============================================================
57 // === OPTIONAL: Internal Relay Operations ===
58 // These are used for relay state management, not NIP-01 queries
59 // ============================================================
60 61 // OPTIONAL (Internal): Marker nodes for tracking relay state
62 // Used for serial number generation, sync markers, etc.
63 "CREATE CONSTRAINT marker_key_unique IF NOT EXISTS FOR (m:Marker) REQUIRE m.key IS UNIQUE",
64 65 // ============================================================
66 // === OPTIONAL: Social Graph Event Processing ===
67 // Tracks processing of social events for graph updates
68 // ============================================================
69 70 // OPTIONAL (Social Graph): Tracks which social events have been processed
71 // Used to build/update WoT graph from kinds 0, 3, 1984, 10000
72 "CREATE CONSTRAINT processedSocialEvent_event_id IF NOT EXISTS FOR (e:ProcessedSocialEvent) REQUIRE e.event_id IS UNIQUE",
73 74 // ============================================================
75 // === OPTIONAL: Web of Trust (WoT) Extension Schema ===
76 // These support trust metrics and social graph analysis
77 // Not required for NIP-01 compliance
78 // ============================================================
79 80 // NOTE: NostrUser constraint is defined above in MANDATORY section
81 // It serves both NIP-01 (author tracking) and WoT (social graph) purposes
82 83 // ============================================================
84 // === OPTIONAL: NIP-50 Word Search Index ===
85 // Supports full-text search via Word nodes linked to events
86 // ============================================================
87 88 // OPTIONAL (NIP-50): Word.hash uniqueness for word search index
89 // Word nodes store 8-byte truncated SHA-256 hashes (hex-encoded) as keys
90 // with the normalized word text as a readable label
91 "CREATE CONSTRAINT word_hash_unique IF NOT EXISTS FOR (w:Word) REQUIRE w.hash IS UNIQUE",
92 93 // OPTIONAL (WoT): Container for WoT metrics cards per observee
94 "CREATE CONSTRAINT setOfNostrUserWotMetricsCards_observee_pubkey IF NOT EXISTS FOR (n:SetOfNostrUserWotMetricsCards) REQUIRE n.observee_pubkey IS UNIQUE",
95 96 // OPTIONAL (WoT): Unique WoT metrics card per customer+observee pair
97 "CREATE CONSTRAINT nostrUserWotMetricsCard_unique_combination_1 IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) REQUIRE (n.customer_id, n.observee_pubkey) IS UNIQUE",
98 99 // OPTIONAL (WoT): Unique WoT metrics card per observer+observee pair
100 "CREATE CONSTRAINT nostrUserWotMetricsCard_unique_combination_2 IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) REQUIRE (n.observer_pubkey, n.observee_pubkey) IS UNIQUE",
101 }
102 103 // Additional indexes for query optimization
104 indexes := []string{
105 // ============================================================
106 // === MANDATORY: NIP-01 REQ Query Indexes ===
107 // These indexes are required for efficient NIP-01 filter execution
108 // ============================================================
109 110 // MANDATORY (NIP-01): Event.kind index for "kinds" filter
111 // REQ filters can specify: {"kinds": [1, 7, ...]}
112 "CREATE INDEX event_kind IF NOT EXISTS FOR (e:Event) ON (e.kind)",
113 114 // MANDATORY (NIP-01): Event.created_at index for "since"/"until" filters
115 // REQ filters can specify: {"since": <timestamp>, "until": <timestamp>}
116 "CREATE INDEX event_created_at IF NOT EXISTS FOR (e:Event) ON (e.created_at)",
117 118 // MANDATORY (NIP-01): Tag.type index for "#<tag>" filter queries
119 // REQ filters can specify: {"#e": ["<event_id>"], "#p": ["<pubkey>"], ...}
120 "CREATE INDEX tag_type IF NOT EXISTS FOR (t:Tag) ON (t.type)",
121 122 // MANDATORY (NIP-01): Tag.value index for "#<tag>" filter queries
123 // Used in conjunction with tag_type for efficient tag lookups
124 "CREATE INDEX tag_value IF NOT EXISTS FOR (t:Tag) ON (t.value)",
125 126 // MANDATORY (NIP-01): Composite tag index for "#<tag>" filter queries
127 // Most efficient for queries like: {"#p": ["<pubkey>"]}
128 "CREATE INDEX tag_type_value IF NOT EXISTS FOR (t:Tag) ON (t.type, t.value)",
129 130 // ============================================================
131 // === RECOMMENDED: Performance Optimization Indexes ===
132 // These improve query performance but aren't strictly required
133 // ============================================================
134 135 // RECOMMENDED: Composite index for common query patterns (kind + created_at)
136 // Optimizes queries like: {"kinds": [1], "since": <ts>, "until": <ts>}
137 "CREATE INDEX event_kind_created_at IF NOT EXISTS FOR (e:Event) ON (e.kind, e.created_at)",
138 139 // ============================================================
140 // === OPTIONAL: Addressable Event Indexes (NIP-33) ===
141 // Support parameterized replaceable events (kinds 30000-39999)
142 // ============================================================
143 144 // OPTIONAL (NIP-33): Event.naddr index for addressable event lookups
145 // Enables fast queries by naddr coordinate (pubkey:kind:dtag)
146 "CREATE INDEX event_naddr IF NOT EXISTS FOR (e:Event) ON (e.naddr)",
147 148 // ============================================================
149 // === OPTIONAL: Internal Relay Operation Indexes ===
150 // Used for relay-internal operations, not NIP-01 queries
151 // ============================================================
152 153 // OPTIONAL (Internal): Event.serial for internal serial-based lookups
154 // Used for cursor-based pagination and sync operations
155 "CREATE INDEX event_serial IF NOT EXISTS FOR (e:Event) ON (e.serial)",
156 157 // OPTIONAL (NIP-40): Event.expiration for expired event cleanup
158 // Used by DeleteExpired to efficiently find events past their expiration time
159 "CREATE INDEX event_expiration IF NOT EXISTS FOR (e:Event) ON (e.expiration)",
160 161 // ============================================================
162 // === OPTIONAL: Social Graph Event Processing Indexes ===
163 // Support tracking of processed social events for graph updates
164 // ============================================================
165 166 // OPTIONAL (Social Graph): Quick lookup of processed events by pubkey+kind
167 "CREATE INDEX processedSocialEvent_pubkey_kind IF NOT EXISTS FOR (e:ProcessedSocialEvent) ON (e.pubkey, e.event_kind)",
168 169 // OPTIONAL (Social Graph): Filter for active (non-superseded) events
170 "CREATE INDEX processedSocialEvent_superseded IF NOT EXISTS FOR (e:ProcessedSocialEvent) ON (e.superseded_by)",
171 172 // ============================================================
173 // === OPTIONAL: Web of Trust (WoT) Extension Indexes ===
174 // These support trust metrics and social graph analysis
175 // Not required for NIP-01 compliance
176 // ============================================================
177 178 // OPTIONAL (WoT): NostrUser trust metric indexes
179 "CREATE INDEX nostrUser_hops IF NOT EXISTS FOR (n:NostrUser) ON (n.hops)",
180 "CREATE INDEX nostrUser_personalizedPageRank IF NOT EXISTS FOR (n:NostrUser) ON (n.personalizedPageRank)",
181 "CREATE INDEX nostrUser_influence IF NOT EXISTS FOR (n:NostrUser) ON (n.influence)",
182 "CREATE INDEX nostrUser_verifiedFollowerCount IF NOT EXISTS FOR (n:NostrUser) ON (n.verifiedFollowerCount)",
183 "CREATE INDEX nostrUser_verifiedMuterCount IF NOT EXISTS FOR (n:NostrUser) ON (n.verifiedMuterCount)",
184 "CREATE INDEX nostrUser_verifiedReporterCount IF NOT EXISTS FOR (n:NostrUser) ON (n.verifiedReporterCount)",
185 "CREATE INDEX nostrUser_followerInput IF NOT EXISTS FOR (n:NostrUser) ON (n.followerInput)",
186 187 // OPTIONAL (WoT): NostrUserWotMetricsCard indexes for trust card lookups
188 "CREATE INDEX nostrUserWotMetricsCard_customer_id IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.customer_id)",
189 "CREATE INDEX nostrUserWotMetricsCard_observer_pubkey IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.observer_pubkey)",
190 "CREATE INDEX nostrUserWotMetricsCard_observee_pubkey IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.observee_pubkey)",
191 "CREATE INDEX nostrUserWotMetricsCard_hops IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.hops)",
192 "CREATE INDEX nostrUserWotMetricsCard_personalizedPageRank IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.personalizedPageRank)",
193 "CREATE INDEX nostrUserWotMetricsCard_influence IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.influence)",
194 "CREATE INDEX nostrUserWotMetricsCard_verifiedFollowerCount IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.verifiedFollowerCount)",
195 "CREATE INDEX nostrUserWotMetricsCard_verifiedMuterCount IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.verifiedMuterCount)",
196 "CREATE INDEX nostrUserWotMetricsCard_verifiedReporterCount IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.verifiedReporterCount)",
197 "CREATE INDEX nostrUserWotMetricsCard_followerInput IF NOT EXISTS FOR (n:NostrUserWotMetricsCard) ON (n.followerInput)",
198 }
199 200 // Execute all constraint creation queries
201 for _, constraint := range constraints {
202 if _, err := n.ExecuteWrite(ctx, constraint, nil); err != nil {
203 return fmt.Errorf("failed to create constraint: %w", err)
204 }
205 }
206 207 // Execute all index creation queries
208 for _, index := range indexes {
209 if _, err := n.ExecuteWrite(ctx, index, nil); err != nil {
210 return fmt.Errorf("failed to create index: %w", err)
211 }
212 }
213 214 n.Logger.Infof("schema applied successfully")
215 return nil
216 }
217 218 // dropAll drops all data from neo4j (useful for testing)
219 func (n *N) dropAll(ctx context.Context) error {
220 n.Logger.Warningf("dropping all data from neo4j")
221 222 // Delete all nodes and relationships
223 _, err := n.ExecuteWrite(ctx, "MATCH (n) DETACH DELETE n", nil)
224 if err != nil {
225 return fmt.Errorf("failed to drop all data: %w", err)
226 }
227 228 // Drop all constraints (MANDATORY + OPTIONAL)
229 constraints := []string{
230 // MANDATORY (NIP-01) constraints
231 "DROP CONSTRAINT event_id_unique IF EXISTS",
232 "DROP CONSTRAINT nostrUser_pubkey IF EXISTS", // Unified author + WoT constraint
233 234 // Legacy constraint (removed in migration)
235 "DROP CONSTRAINT author_pubkey_unique IF EXISTS",
236 237 // OPTIONAL (NIP-33) constraints
238 "DROP CONSTRAINT naddr_unique IF EXISTS",
239 240 // OPTIONAL (Internal) constraints
241 "DROP CONSTRAINT marker_key_unique IF EXISTS",
242 243 // OPTIONAL (NIP-50) constraints
244 "DROP CONSTRAINT word_hash_unique IF EXISTS",
245 246 // OPTIONAL (Social Graph) constraints
247 "DROP CONSTRAINT processedSocialEvent_event_id IF EXISTS",
248 "DROP CONSTRAINT setOfNostrUserWotMetricsCards_observee_pubkey IF EXISTS",
249 "DROP CONSTRAINT nostrUserWotMetricsCard_unique_combination_1 IF EXISTS",
250 "DROP CONSTRAINT nostrUserWotMetricsCard_unique_combination_2 IF EXISTS",
251 }
252 253 for _, constraint := range constraints {
254 _, _ = n.ExecuteWrite(ctx, constraint, nil)
255 // Ignore errors as constraints may not exist
256 }
257 258 // Drop all indexes (MANDATORY + RECOMMENDED + OPTIONAL)
259 indexes := []string{
260 // MANDATORY (NIP-01) indexes
261 "DROP INDEX event_kind IF EXISTS",
262 "DROP INDEX event_created_at IF EXISTS",
263 "DROP INDEX tag_type IF EXISTS",
264 "DROP INDEX tag_value IF EXISTS",
265 "DROP INDEX tag_type_value IF EXISTS",
266 267 // RECOMMENDED (Performance) indexes
268 "DROP INDEX event_kind_created_at IF EXISTS",
269 270 // OPTIONAL (NIP-33) indexes
271 "DROP INDEX event_naddr IF EXISTS",
272 273 // OPTIONAL (Internal) indexes
274 "DROP INDEX event_serial IF EXISTS",
275 "DROP INDEX event_expiration IF EXISTS",
276 277 // OPTIONAL (Social Graph) indexes
278 "DROP INDEX processedSocialEvent_pubkey_kind IF EXISTS",
279 "DROP INDEX processedSocialEvent_superseded IF EXISTS",
280 281 // OPTIONAL (WoT) indexes
282 "DROP INDEX nostrUser_hops IF EXISTS",
283 "DROP INDEX nostrUser_personalizedPageRank IF EXISTS",
284 "DROP INDEX nostrUser_influence IF EXISTS",
285 "DROP INDEX nostrUser_verifiedFollowerCount IF EXISTS",
286 "DROP INDEX nostrUser_verifiedMuterCount IF EXISTS",
287 "DROP INDEX nostrUser_verifiedReporterCount IF EXISTS",
288 "DROP INDEX nostrUser_followerInput IF EXISTS",
289 "DROP INDEX nostrUserWotMetricsCard_customer_id IF EXISTS",
290 "DROP INDEX nostrUserWotMetricsCard_observer_pubkey IF EXISTS",
291 "DROP INDEX nostrUserWotMetricsCard_observee_pubkey IF EXISTS",
292 "DROP INDEX nostrUserWotMetricsCard_hops IF EXISTS",
293 "DROP INDEX nostrUserWotMetricsCard_personalizedPageRank IF EXISTS",
294 "DROP INDEX nostrUserWotMetricsCard_influence IF EXISTS",
295 "DROP INDEX nostrUserWotMetricsCard_verifiedFollowerCount IF EXISTS",
296 "DROP INDEX nostrUserWotMetricsCard_verifiedMuterCount IF EXISTS",
297 "DROP INDEX nostrUserWotMetricsCard_verifiedReporterCount IF EXISTS",
298 "DROP INDEX nostrUserWotMetricsCard_followerInput IF EXISTS",
299 }
300 301 for _, index := range indexes {
302 _, _ = n.ExecuteWrite(ctx, index, nil)
303 // Ignore errors as indexes may not exist
304 }
305 306 // Reapply schema after dropping
307 return n.applySchema(ctx)
308 }
309