schema.go raw

   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