Browse Source

Add naddr property to Neo4j addressable Event nodes (v0.58.0)

- Add naddr_unique constraint and event_naddr index to schema.go
- Add buildNaddr() helper to compute pubkey:kind:dtag coordinate
- Store naddr on Event nodes for kinds 30000-39999, NULL for others
- Add v5 migration to populate naddr for existing addressable events

Implements: https://git.nostrdev.com/mleku/next.orly.dev/issues/27

Files modified:
- pkg/neo4j/schema.go: Added naddr constraint/index and DROP statements
- pkg/neo4j/save-event.go: Added buildNaddr() and naddr in Event CREATE
- pkg/neo4j/migrations.go: Added v5 migration migrateAddNaddr()
- pkg/version/version: Bumped to v0.58.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
main v0.58.0
woikos 4 months ago
parent
commit
c0e92a2dd2
No known key found for this signature in database
  1. 80
      pkg/neo4j/migrations.go
  2. 39
      pkg/neo4j/save-event.go
  3. 25
      pkg/neo4j/schema.go
  4. 2
      pkg/version/version

80
pkg/neo4j/migrations.go

@ -35,6 +35,11 @@ var migrations = []Migration{
Description: "Deduplicate REPORTS relationships by (reporter, reported, report_type)", Description: "Deduplicate REPORTS relationships by (reporter, reported, report_type)",
Migrate: migrateDeduplicateReports, Migrate: migrateDeduplicateReports,
}, },
{
Version: "v5",
Description: "Add naddr property to addressable Event nodes (kinds 30000-39999)",
Migrate: migrateAddNaddr,
},
} }
// RunMigrations executes all pending migrations // RunMigrations executes all pending migrations
@ -595,3 +600,78 @@ func migrateDeduplicateReports(ctx context.Context, n *N) error {
n.Logger.Infof("REPORTS deduplication migration completed successfully") n.Logger.Infof("REPORTS deduplication migration completed successfully")
return nil return nil
} }
// migrateAddNaddr adds the naddr property to existing addressable Event nodes (kinds 30000-39999).
// The naddr format is: pubkey:kind:dtag (colon-delimited coordinate)
// This enables direct lookups and uniqueness constraints for parameterized replaceable events.
func migrateAddNaddr(ctx context.Context, n *N) error {
// Step 1: Count addressable events without naddr
countCypher := `
MATCH (e:Event)
WHERE e.kind >= 30000 AND e.kind < 40000
AND e.naddr IS NULL
RETURN count(e) AS count
`
result, err := n.ExecuteRead(ctx, countCypher, nil)
if err != nil {
return fmt.Errorf("failed to count addressable events: %w", err)
}
var eventCount int64
if result.Next(ctx) {
if count, ok := result.Record().Values[0].(int64); ok {
eventCount = count
}
}
if eventCount == 0 {
n.Logger.Infof("no addressable events without naddr found, migration complete")
return nil
}
n.Logger.Infof("found %d addressable events to update with naddr", eventCount)
// Step 2: Update events in batches
// For each event, compute naddr from pubkey + kind + d-tag
// The d-tag value is obtained from the Tag node via TAGGED_WITH relationship
updateCypher := `
MATCH (e:Event)
WHERE e.kind >= 30000 AND e.kind < 40000
AND e.naddr IS NULL
WITH e LIMIT 1000
// Get d-tag value via TAGGED_WITH relationship
OPTIONAL MATCH (e)-[:TAGGED_WITH]->(t:Tag {type: 'd'})
WITH e, COALESCE(t.value, '') AS dValue
// Build naddr: pubkey:kind:dValue
SET e.naddr = e.pubkey + ':' + toString(e.kind) + ':' + dValue
RETURN count(e) AS updated
`
// Run migration in batches until no more events to update
totalUpdated := int64(0)
for {
writeResult, err := n.ExecuteWrite(ctx, updateCypher, nil)
if err != nil {
return fmt.Errorf("failed to update addressable events batch: %w", err)
}
var batchUpdated int64
if writeResult.Next(ctx) {
if count, ok := writeResult.Record().Values[0].(int64); ok {
batchUpdated = count
}
}
if batchUpdated == 0 {
break
}
totalUpdated += batchUpdated
n.Logger.Infof("updated %d addressable events with naddr (total: %d)", batchUpdated, totalUpdated)
}
n.Logger.Infof("naddr migration completed: updated %d addressable events", totalUpdated)
return nil
}

39
pkg/neo4j/save-event.go

@ -111,6 +111,30 @@ func safePrefix(s string, n int) string {
return s[:n] return s[:n]
} }
// buildNaddr creates the naddr coordinate string for addressable events.
// Format: pubkey:kind:dtag (colon-delimited)
// Returns empty string for non-addressable events.
// This is used for the naddr_unique constraint in Neo4j.
func buildNaddr(ev *event.E) string {
// Only for addressable events (kinds 30000-39999)
if ev.Kind < 30000 || ev.Kind >= 40000 {
return ""
}
pubkey := hex.Enc(ev.Pubkey[:])
kind := strconv.FormatInt(int64(ev.Kind), 10)
// Get d-tag value (empty string if not present)
dValue := ""
if ev.Tags != nil {
if dTag := ev.Tags.GetFirst([]byte{'d'}); dTag != nil && len(dTag.T) >= 2 {
dValue = string(dTag.T[1])
}
}
return pubkey + ":" + kind + ":" + dValue
}
// buildBaseEventCypher constructs a Cypher query to create just the base event node and author. // buildBaseEventCypher constructs a Cypher query to create just the base event node and author.
// Tags are added separately in batches to prevent stack overflow with large tag sets. // Tags are added separately in batches to prevent stack overflow with large tag sets.
// This creates: // This creates:
@ -142,6 +166,16 @@ func (n *N) buildBaseEventCypher(ev *event.E, serial uint64) (string, map[string
} }
params["expiration"] = expirationTs params["expiration"] = expirationTs
// Compute naddr for addressable events (kinds 30000-39999)
// Format: pubkey:kind:dtag - NULL for non-addressable events
// NULL allows multiple non-addressable events while maintaining uniqueness for naddr values
naddr := buildNaddr(ev)
if naddr != "" {
params["naddr"] = naddr
} else {
params["naddr"] = nil
}
// Serialize tags as JSON string for storage // Serialize tags as JSON string for storage
// Handle nil tags gracefully - nil means empty tags "[]" // Handle nil tags gracefully - nil means empty tags "[]"
var tagsJSON []byte var tagsJSON []byte
@ -160,7 +194,7 @@ func (n *N) buildBaseEventCypher(ev *event.E, serial uint64) (string, map[string
MERGE (a:NostrUser {pubkey: $pubkey}) MERGE (a:NostrUser {pubkey: $pubkey})
ON CREATE SET a.created_at = timestamp(), a.first_seen_event = $eventId ON CREATE SET a.created_at = timestamp(), a.first_seen_event = $eventId
// Create event node with expiration for NIP-40 support // Create event node with expiration for NIP-40 support and naddr for NIP-33 addressable events
CREATE (e:Event { CREATE (e:Event {
id: $eventId, id: $eventId,
serial: $serial, serial: $serial,
@ -170,7 +204,8 @@ CREATE (e:Event {
sig: $sig, sig: $sig,
pubkey: $pubkey, pubkey: $pubkey,
tags: $tags, tags: $tags,
expiration: $expiration expiration: $expiration,
naddr: $naddr
}) })
// Link event to author // Link event to author

25
pkg/neo4j/schema.go

@ -43,6 +43,16 @@ func (n *N) applySchema(ctx context.Context) error {
// NOTE: NostrUser unifies both NIP-01 author tracking and WoT social graph // NOTE: NostrUser unifies both NIP-01 author tracking and WoT social graph
"CREATE CONSTRAINT nostrUser_pubkey IF NOT EXISTS FOR (n:NostrUser) REQUIRE n.pubkey IS UNIQUE", "CREATE CONSTRAINT nostrUser_pubkey IF NOT EXISTS FOR (n:NostrUser) REQUIRE n.pubkey IS UNIQUE",
// ============================================================
// === OPTIONAL: Addressable Event Support (NIP-33) ===
// These support parameterized replaceable events (kinds 30000-39999)
// ============================================================
// OPTIONAL (NIP-33): naddr uniqueness for addressable events
// Format: pubkey:kind:dtag (colon-delimited coordinate)
// Ensures only one event per author+kind+d-tag combination
"CREATE CONSTRAINT naddr_unique IF NOT EXISTS FOR (e:Event) REQUIRE e.naddr IS UNIQUE",
// ============================================================ // ============================================================
// === OPTIONAL: Internal Relay Operations === // === OPTIONAL: Internal Relay Operations ===
// These are used for relay state management, not NIP-01 queries // These are used for relay state management, not NIP-01 queries
@ -116,6 +126,15 @@ func (n *N) applySchema(ctx context.Context) error {
// Optimizes queries like: {"kinds": [1], "since": <ts>, "until": <ts>} // Optimizes queries like: {"kinds": [1], "since": <ts>, "until": <ts>}
"CREATE INDEX event_kind_created_at IF NOT EXISTS FOR (e:Event) ON (e.kind, e.created_at)", "CREATE INDEX event_kind_created_at IF NOT EXISTS FOR (e:Event) ON (e.kind, e.created_at)",
// ============================================================
// === OPTIONAL: Addressable Event Indexes (NIP-33) ===
// Support parameterized replaceable events (kinds 30000-39999)
// ============================================================
// OPTIONAL (NIP-33): Event.naddr index for addressable event lookups
// Enables fast queries by naddr coordinate (pubkey:kind:dtag)
"CREATE INDEX event_naddr IF NOT EXISTS FOR (e:Event) ON (e.naddr)",
// ============================================================ // ============================================================
// === OPTIONAL: Internal Relay Operation Indexes === // === OPTIONAL: Internal Relay Operation Indexes ===
// Used for relay-internal operations, not NIP-01 queries // Used for relay-internal operations, not NIP-01 queries
@ -205,6 +224,9 @@ func (n *N) dropAll(ctx context.Context) error {
// Legacy constraint (removed in migration) // Legacy constraint (removed in migration)
"DROP CONSTRAINT author_pubkey_unique IF EXISTS", "DROP CONSTRAINT author_pubkey_unique IF EXISTS",
// OPTIONAL (NIP-33) constraints
"DROP CONSTRAINT naddr_unique IF EXISTS",
// OPTIONAL (Internal) constraints // OPTIONAL (Internal) constraints
"DROP CONSTRAINT marker_key_unique IF EXISTS", "DROP CONSTRAINT marker_key_unique IF EXISTS",
@ -232,6 +254,9 @@ func (n *N) dropAll(ctx context.Context) error {
// RECOMMENDED (Performance) indexes // RECOMMENDED (Performance) indexes
"DROP INDEX event_kind_created_at IF EXISTS", "DROP INDEX event_kind_created_at IF EXISTS",
// OPTIONAL (NIP-33) indexes
"DROP INDEX event_naddr IF EXISTS",
// OPTIONAL (Internal) indexes // OPTIONAL (Internal) indexes
"DROP INDEX event_serial IF EXISTS", "DROP INDEX event_serial IF EXISTS",
"DROP INDEX event_expiration IF EXISTS", "DROP INDEX event_expiration IF EXISTS",

2
pkg/version/version

@ -1 +1 @@
v0.57.2 v0.58.0

Loading…
Cancel
Save