From 64c6bd8bdd98a9c5f436fe5550cc6a1218cd3353 Mon Sep 17 00:00:00 2001 From: mleku Date: Wed, 3 Dec 2025 10:25:31 +0000 Subject: [PATCH] add cypher to cloud skills --- .claude/settings.local.json | 14 +- .claude/skills/cypher/SKILL.md | 395 +++++++++++++ .../cypher/references/common-mistakes.md | 381 ++++++++++++ .../cypher/references/common-patterns.md | 397 +++++++++++++ .../cypher/references/syntax-reference.md | 540 ++++++++++++++++++ 5 files changed, 1726 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/cypher/SKILL.md create mode 100644 .claude/skills/cypher/references/common-mistakes.md create mode 100644 .claude/skills/cypher/references/common-patterns.md create mode 100644 .claude/skills/cypher/references/syntax-reference.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 00ecb50..d97d4c8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -155,7 +155,19 @@ "WebFetch(domain:www.npmjs.com)", "Bash(git stash:*)", "WebFetch(domain:arxiv.org)", - "WebFetch(domain:hal.science)" + "WebFetch(domain:hal.science)", + "WebFetch(domain:pkg.go.dev)", + "Bash(GOOS=js GOARCH=wasm CGO_ENABLED=0 go build:*)", + "Bash(GOOS=js GOARCH=wasm go doc:*)", + "Bash(GOOS=js GOARCH=wasm CGO_ENABLED=0 go test:*)", + "Bash(node --version:*)", + "Bash(npm install)", + "Bash(node run_wasm_tests.mjs:*)", + "Bash(go env:*)", + "Bash(GOROOT=/home/mleku/go node run_wasm_tests.mjs:*)", + "Bash(./orly:*)", + "Bash(./orly -version:*)", + "Bash(./orly --version:*)" ], "deny": [], "ask": [] diff --git a/.claude/skills/cypher/SKILL.md b/.claude/skills/cypher/SKILL.md new file mode 100644 index 0000000..e8ae91b --- /dev/null +++ b/.claude/skills/cypher/SKILL.md @@ -0,0 +1,395 @@ +--- +name: cypher +description: This skill should be used when writing, debugging, or discussing Neo4j Cypher queries. Provides comprehensive knowledge of Cypher syntax, query patterns, performance optimization, and common mistakes. Particularly useful for translating between domain models and graph queries. +--- + +# Neo4j Cypher Query Language + +## Purpose + +This skill provides expert-level guidance for writing Neo4j Cypher queries, including syntax, patterns, performance optimization, and common pitfalls. It is particularly tuned for the patterns used in this ORLY Nostr relay codebase. + +## When to Use + +Activate this skill when: +- Writing Cypher queries for Neo4j +- Debugging Cypher syntax errors +- Optimizing query performance +- Translating Nostr filter queries to Cypher +- Working with graph relationships and traversals +- Creating or modifying schema (indexes, constraints) + +## Core Cypher Syntax + +### Clause Order (CRITICAL) + +Cypher requires clauses in a specific order. Violating this causes syntax errors: + +```cypher +// CORRECT order of clauses +MATCH (n:Label) // 1. Pattern matching +WHERE n.prop = value // 2. Filtering +WITH n, count(*) AS cnt // 3. Intermediate results (resets scope) +OPTIONAL MATCH (n)-[r]-() // 4. Optional patterns +CREATE (m:NewNode) // 5. Node/relationship creation +SET n.prop = value // 6. Property updates +DELETE r // 7. Deletions +RETURN n.prop AS result // 8. Return clause +ORDER BY result DESC // 9. Ordering +SKIP 10 LIMIT 20 // 10. Pagination +``` + +### The WITH Clause (CRITICAL) + +The `WITH` clause is required to transition between certain operations: + +**Rule: Cannot use MATCH after CREATE without WITH** + +```cypher +// WRONG - MATCH after CREATE without WITH +CREATE (e:Event {id: $id}) +MATCH (ref:Event {id: $refId}) // ERROR! +CREATE (e)-[:REFERENCES]->(ref) + +// CORRECT - Use WITH to carry variables forward +CREATE (e:Event {id: $id}) +WITH e +MATCH (ref:Event {id: $refId}) +CREATE (e)-[:REFERENCES]->(ref) +``` + +**Rule: WITH resets the scope** + +Variables not included in WITH are no longer accessible: + +```cypher +// WRONG - 'a' is lost after WITH +MATCH (a:Author), (e:Event) +WITH e +WHERE a.pubkey = $pubkey // ERROR: 'a' not defined + +// CORRECT - Include all needed variables +MATCH (a:Author), (e:Event) +WITH a, e +WHERE a.pubkey = $pubkey +``` + +### Node and Relationship Patterns + +```cypher +// Nodes +(n) // Anonymous node +(n:Label) // Labeled node +(n:Label {prop: value}) // Node with properties +(n:Label:OtherLabel) // Multiple labels + +// Relationships +-[r]-> // Directed, anonymous +-[r:TYPE]-> // Typed relationship +-[r:TYPE {prop: value}]-> // With properties +-[r:TYPE|OTHER]-> // Multiple types (OR) +-[*1..3]-> // Variable length (1 to 3 hops) +-[*]-> // Any number of hops +``` + +### MERGE vs CREATE + +**CREATE**: Always creates new nodes/relationships (may create duplicates) + +```cypher +CREATE (n:Event {id: $id}) // Creates even if id exists +``` + +**MERGE**: Finds or creates (idempotent) + +```cypher +MERGE (n:Event {id: $id}) // Finds existing or creates new +ON CREATE SET n.created = timestamp() +ON MATCH SET n.accessed = timestamp() +``` + +**Best Practice**: Use MERGE for reference nodes, CREATE for unique events + +```cypher +// Reference nodes - use MERGE (idempotent) +MERGE (author:Author {pubkey: $pubkey}) + +// Unique events - use CREATE (after checking existence) +CREATE (e:Event {id: $eventId, ...}) +``` + +### OPTIONAL MATCH + +Returns NULL for non-matching patterns (like LEFT JOIN): + +```cypher +// Find events, with or without tags +MATCH (e:Event) +OPTIONAL MATCH (e)-[:TAGGED_WITH]->(t:Tag) +RETURN e.id, collect(t.value) AS tags +``` + +### Conditional Creation with FOREACH + +To conditionally create relationships: + +```cypher +// FOREACH trick for conditional operations +OPTIONAL MATCH (ref:Event {id: $refId}) +FOREACH (ignoreMe IN CASE WHEN ref IS NOT NULL THEN [1] ELSE [] END | + CREATE (e)-[:REFERENCES]->(ref) +) +``` + +### Aggregation Functions + +```cypher +count(*) // Count all rows +count(n) // Count non-null values +count(DISTINCT n) // Count unique values +collect(n) // Collect into list +collect(DISTINCT n) // Collect unique values +sum(n.value) // Sum values +avg(n.value) // Average +min(n.value), max(n.value) // Min/max +``` + +### String Operations + +```cypher +// String matching +WHERE n.name STARTS WITH 'prefix' +WHERE n.name ENDS WITH 'suffix' +WHERE n.name CONTAINS 'substring' +WHERE n.name =~ 'regex.*pattern' // Regex + +// String functions +toLower(str), toUpper(str) +trim(str), ltrim(str), rtrim(str) +substring(str, start, length) +replace(str, search, replacement) +``` + +### List Operations + +```cypher +// IN clause +WHERE n.kind IN [1, 7, 30023] +WHERE n.pubkey IN $pubkeyList + +// List comprehension +[x IN list WHERE x > 0 | x * 2] + +// UNWIND - expand list into rows +UNWIND $pubkeys AS pubkey +MERGE (u:User {pubkey: pubkey}) +``` + +### Parameters + +Always use parameters for values (security + performance): + +```cypher +// CORRECT - parameterized +MATCH (e:Event {id: $eventId}) +WHERE e.kind IN $kinds + +// WRONG - string interpolation (SQL injection risk!) +MATCH (e:Event {id: '" + eventId + "'}) +``` + +## Schema Management + +### Constraints + +```cypher +// Uniqueness constraint (also creates index) +CREATE CONSTRAINT event_id_unique IF NOT EXISTS +FOR (e:Event) REQUIRE e.id IS UNIQUE + +// Composite uniqueness +CREATE CONSTRAINT card_unique IF NOT EXISTS +FOR (c:Card) REQUIRE (c.customer_id, c.observee_pubkey) IS UNIQUE + +// Drop constraint +DROP CONSTRAINT event_id_unique IF EXISTS +``` + +### Indexes + +```cypher +// Single property index +CREATE INDEX event_kind IF NOT EXISTS FOR (e:Event) ON (e.kind) + +// Composite index +CREATE INDEX event_kind_created IF NOT EXISTS +FOR (e:Event) ON (e.kind, e.created_at) + +// Drop index +DROP INDEX event_kind IF EXISTS +``` + +## Common Query Patterns + +### Find with Filter + +```cypher +// Multiple conditions with OR +MATCH (e:Event) +WHERE e.kind IN $kinds + AND (e.id = $id1 OR e.id = $id2) + AND e.created_at >= $since +RETURN e +ORDER BY e.created_at DESC +LIMIT $limit +``` + +### Graph Traversal + +```cypher +// Find events by author +MATCH (e:Event)-[:AUTHORED_BY]->(a:Author {pubkey: $pubkey}) +RETURN e + +// Find followers of a user +MATCH (follower:NostrUser)-[:FOLLOWS]->(user:NostrUser {pubkey: $pubkey}) +RETURN follower.pubkey + +// Find mutual follows (friends) +MATCH (a:NostrUser {pubkey: $pubkeyA})-[:FOLLOWS]->(b:NostrUser) +WHERE (b)-[:FOLLOWS]->(a) +RETURN b.pubkey AS mutual_friend +``` + +### Upsert Pattern + +```cypher +MERGE (n:Node {key: $key}) +ON CREATE SET + n.created_at = timestamp(), + n.value = $value +ON MATCH SET + n.updated_at = timestamp(), + n.value = $value +RETURN n +``` + +### Batch Processing with UNWIND + +```cypher +// Create multiple nodes from list +UNWIND $items AS item +CREATE (n:Node {id: item.id, value: item.value}) + +// Create relationships from list +UNWIND $follows AS followed_pubkey +MERGE (followed:NostrUser {pubkey: followed_pubkey}) +MERGE (author)-[:FOLLOWS]->(followed) +``` + +## Performance Optimization + +### Index Usage + +1. **Start with indexed properties** - Begin MATCH with most selective indexed field +2. **Use composite indexes** - For queries filtering on multiple properties +3. **Profile queries** - Use `PROFILE` prefix to see execution plan + +```cypher +PROFILE MATCH (e:Event {kind: 1}) +WHERE e.created_at > $since +RETURN e LIMIT 100 +``` + +### Query Optimization Tips + +1. **Filter early** - Put WHERE conditions close to MATCH +2. **Limit early** - Use LIMIT as early as possible +3. **Avoid Cartesian products** - Connect patterns or use WITH +4. **Use parameters** - Enables query plan caching + +```cypher +// GOOD - Filter and limit early +MATCH (e:Event) +WHERE e.kind IN $kinds AND e.created_at >= $since +WITH e ORDER BY e.created_at DESC LIMIT 100 +OPTIONAL MATCH (e)-[:TAGGED_WITH]->(t:Tag) +RETURN e, collect(t) + +// BAD - Late filtering +MATCH (e:Event), (t:Tag) +WHERE e.kind IN $kinds +RETURN e, t LIMIT 100 +``` + +## Reference Materials + +For detailed information, consult the reference files: + +- **references/syntax-reference.md** - Complete Cypher syntax guide with all clause types, operators, and functions +- **references/common-patterns.md** - Project-specific patterns for ORLY Nostr relay including event storage, tag queries, and social graph traversals +- **references/common-mistakes.md** - Frequent Cypher errors and how to avoid them + +## ORLY-Specific Patterns + +This codebase uses these specific Cypher patterns: + +### Event Storage Pattern + +```cypher +// Create event with author relationship +MERGE (a:Author {pubkey: $pubkey}) +CREATE (e:Event { + id: $eventId, + serial: $serial, + kind: $kind, + created_at: $createdAt, + content: $content, + sig: $sig, + pubkey: $pubkey, + tags: $tags +}) +CREATE (e)-[:AUTHORED_BY]->(a) +``` + +### Tag Query Pattern + +```cypher +// Query events by tag (Nostr # filter) +MATCH (e:Event)-[:TAGGED_WITH]->(t:Tag {type: $tagType}) +WHERE t.value IN $tagValues +RETURN e +ORDER BY e.created_at DESC +LIMIT $limit +``` + +### Social Graph Pattern + +```cypher +// Process contact list with diff-based updates +// Mark old as superseded +OPTIONAL MATCH (old:ProcessedSocialEvent {event_id: $old_event_id}) +SET old.superseded_by = $new_event_id + +// Create tracking node +CREATE (new:ProcessedSocialEvent { + event_id: $new_event_id, + event_kind: 3, + pubkey: $author_pubkey, + created_at: $created_at, + processed_at: timestamp() +}) + +// Update relationships +MERGE (author:NostrUser {pubkey: $author_pubkey}) +WITH author +UNWIND $added_follows AS followed_pubkey +MERGE (followed:NostrUser {pubkey: followed_pubkey}) +MERGE (author)-[:FOLLOWS]->(followed) +``` + +## Official Resources + +- Neo4j Cypher Manual: https://neo4j.com/docs/cypher-manual/current/ +- Cypher Cheat Sheet: https://neo4j.com/docs/cypher-cheat-sheet/current/ +- Query Tuning: https://neo4j.com/docs/cypher-manual/current/query-tuning/ \ No newline at end of file diff --git a/.claude/skills/cypher/references/common-mistakes.md b/.claude/skills/cypher/references/common-mistakes.md new file mode 100644 index 0000000..a61efe9 --- /dev/null +++ b/.claude/skills/cypher/references/common-mistakes.md @@ -0,0 +1,381 @@ +# Common Cypher Mistakes and How to Avoid Them + +## Clause Ordering Errors + +### MATCH After CREATE Without WITH + +**Error**: `Invalid input 'MATCH': expected ... WITH` + +```cypher +// WRONG +CREATE (e:Event {id: $id}) +MATCH (ref:Event {id: $refId}) // ERROR! +CREATE (e)-[:REFERENCES]->(ref) + +// CORRECT - Use WITH to transition +CREATE (e:Event {id: $id}) +WITH e +MATCH (ref:Event {id: $refId}) +CREATE (e)-[:REFERENCES]->(ref) +``` + +**Rule**: After CREATE, you must use WITH before MATCH. + +### WHERE After WITH Without Carrying Variables + +**Error**: `Variable 'x' not defined` + +```cypher +// WRONG - 'a' is lost +MATCH (a:Author), (e:Event) +WITH e +WHERE a.pubkey = $pubkey // ERROR: 'a' not in scope + +// CORRECT - Include all needed variables +MATCH (a:Author), (e:Event) +WITH a, e +WHERE a.pubkey = $pubkey +``` + +**Rule**: WITH resets the scope. Include all variables you need. + +### ORDER BY Without Aliased Return + +**Error**: `Invalid input 'ORDER': expected ... AS` + +```cypher +// WRONG in some contexts +RETURN n.name +ORDER BY n.name + +// SAFER - Use alias +RETURN n.name AS name +ORDER BY name +``` + +## MERGE Mistakes + +### MERGE on Complex Pattern Creates Duplicates + +```cypher +// DANGEROUS - May create duplicate nodes +MERGE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'}) + +// CORRECT - MERGE nodes separately first +MERGE (a:Person {name: 'Alice'}) +MERGE (b:Person {name: 'Bob'}) +MERGE (a)-[:KNOWS]->(b) +``` + +**Rule**: MERGE simple patterns, not complex ones. + +### MERGE Without Unique Property + +```cypher +// DANGEROUS - Will keep creating nodes +MERGE (p:Person) // No unique identifier! +SET p.name = 'Alice' + +// CORRECT - Provide unique key +MERGE (p:Person {email: $email}) +SET p.name = 'Alice' +``` + +**Rule**: MERGE must have properties that uniquely identify the node. + +### Missing ON CREATE/ON MATCH + +```cypher +// LOSES context of whether new or existing +MERGE (p:Person {id: $id}) +SET p.updated_at = timestamp() // Always runs + +// BETTER - Handle each case +MERGE (p:Person {id: $id}) +ON CREATE SET p.created_at = timestamp() +ON MATCH SET p.updated_at = timestamp() +``` + +## NULL Handling Errors + +### Comparing with NULL + +```cypher +// WRONG - NULL = NULL is NULL, not true +WHERE n.email = null // Never matches! + +// CORRECT +WHERE n.email IS NULL +WHERE n.email IS NOT NULL +``` + +### NULL in Aggregations + +```cypher +// count(NULL) returns 0, collect(NULL) includes NULL +MATCH (n:Person) +OPTIONAL MATCH (n)-[:BOUGHT]->(p:Product) +RETURN n.name, count(p) // count ignores NULL +``` + +### NULL Propagation in Expressions + +```cypher +// Any operation with NULL returns NULL +WHERE n.age + 1 > 21 // If n.age is NULL, whole expression is NULL (falsy) + +// Handle with coalesce +WHERE coalesce(n.age, 0) + 1 > 21 +``` + +## List and IN Clause Errors + +### Empty List in IN + +```cypher +// An empty list never matches +WHERE n.kind IN [] // Always false + +// Check for empty list in application code before query +// Or use CASE: +WHERE CASE WHEN size($kinds) > 0 THEN n.kind IN $kinds ELSE true END +``` + +### IN with NULL Values + +```cypher +// NULL in the list causes issues +WHERE n.id IN [1, NULL, 3] // NULL is never equal to anything + +// Filter NULLs in application code +``` + +## Relationship Pattern Errors + +### Forgetting Direction + +```cypher +// WRONG - Creates both directions +MATCH (a)-[:FOLLOWS]-(b) // Undirected! + +// CORRECT - Specify direction +MATCH (a)-[:FOLLOWS]->(b) // a follows b +MATCH (a)<-[:FOLLOWS]-(b) // b follows a +``` + +### Variable-Length Without Bounds + +```cypher +// DANGEROUS - Potentially explosive +MATCH (a)-[*]->(b) // Any length path! + +// SAFE - Set bounds +MATCH (a)-[*1..3]->(b) // 1 to 3 hops max +``` + +### Creating Duplicate Relationships + +```cypher +// May create duplicates +CREATE (a)-[:KNOWS]->(b) + +// Idempotent +MERGE (a)-[:KNOWS]->(b) +``` + +## Performance Mistakes + +### Cartesian Products + +```cypher +// WRONG - Cartesian product +MATCH (a:Person), (b:Product) +WHERE a.id = $personId AND b.id = $productId +CREATE (a)-[:BOUGHT]->(b) + +// CORRECT - Single pattern or sequential +MATCH (a:Person {id: $personId}) +MATCH (b:Product {id: $productId}) +CREATE (a)-[:BOUGHT]->(b) +``` + +### Late Filtering + +```cypher +// SLOW - Filters after collecting everything +MATCH (e:Event) +WITH e +WHERE e.kind = 1 // Should be in MATCH or right after + +// FAST - Filter early +MATCH (e:Event) +WHERE e.kind = 1 +``` + +### Missing LIMIT with ORDER BY + +```cypher +// SLOW - Sorts all results +MATCH (e:Event) +RETURN e +ORDER BY e.created_at DESC + +// FAST - Limits result set +MATCH (e:Event) +RETURN e +ORDER BY e.created_at DESC +LIMIT 100 +``` + +### Unparameterized Queries + +```cypher +// WRONG - No query plan caching, injection risk +MATCH (e:Event {id: '" + eventId + "'}) + +// CORRECT - Use parameters +MATCH (e:Event {id: $eventId}) +``` + +## String Comparison Errors + +### Case Sensitivity + +```cypher +// Cypher strings are case-sensitive +WHERE n.name = 'alice' // Won't match 'Alice' + +// Use toLower/toUpper for case-insensitive +WHERE toLower(n.name) = toLower($name) + +// Or use regex with (?i) +WHERE n.name =~ '(?i)alice' +``` + +### LIKE vs CONTAINS + +```cypher +// There's no LIKE in Cypher +WHERE n.name LIKE '%alice%' // ERROR! + +// Use CONTAINS, STARTS WITH, ENDS WITH +WHERE n.name CONTAINS 'alice' +WHERE n.name STARTS WITH 'ali' +WHERE n.name ENDS WITH 'ice' + +// Or regex for complex patterns +WHERE n.name =~ '.*ali.*ce.*' +``` + +## Index Mistakes + +### Constraint vs Index + +```cypher +// Constraint (also creates index, enforces uniqueness) +CREATE CONSTRAINT foo IF NOT EXISTS FOR (n:Node) REQUIRE n.id IS UNIQUE + +// Index only (no uniqueness enforcement) +CREATE INDEX bar IF NOT EXISTS FOR (n:Node) ON (n.id) +``` + +### Index Not Used + +```cypher +// Index on n.id won't help here +WHERE toLower(n.id) = $id // Function applied to indexed property! + +// Store lowercase if needed, or create computed property +``` + +### Wrong Composite Index Order + +```cypher +// Index on (kind, created_at) won't help query by created_at alone +MATCH (e:Event) WHERE e.created_at > $since // Index not used + +// Either create single-property index or query by kind too +CREATE INDEX event_created_at FOR (e:Event) ON (e.created_at) +``` + +## Transaction Errors + +### Read After Write in Same Transaction + +```cypher +// In Neo4j, reads in a transaction see the writes +// But be careful with external processes +CREATE (n:Node {id: 'new'}) +WITH n +MATCH (m:Node {id: 'new'}) // Will find 'n' +``` + +### Locks and Deadlocks + +```cypher +// MERGE takes locks; avoid complex patterns that might deadlock +// Bad: two MERGEs on same labels in different order +Session 1: MERGE (a:Person {id: 1}) MERGE (b:Person {id: 2}) +Session 2: MERGE (b:Person {id: 2}) MERGE (a:Person {id: 1}) // Potential deadlock + +// Good: consistent ordering +Session 1: MERGE (a:Person {id: 1}) MERGE (b:Person {id: 2}) +Session 2: MERGE (a:Person {id: 1}) MERGE (b:Person {id: 2}) +``` + +## Type Coercion Issues + +### Integer vs String + +```cypher +// Types must match +WHERE n.id = 123 // Won't match if n.id is "123" +WHERE n.id = '123' // Won't match if n.id is 123 + +// Use appropriate parameter types from Go +params["id"] = int64(123) // For integer +params["id"] = "123" // For string +``` + +### Boolean Handling + +```cypher +// Neo4j booleans vs strings +WHERE n.active = true // Boolean +WHERE n.active = 'true' // String - different! +``` + +## Delete Errors + +### Delete Node With Relationships + +```cypher +// ERROR - Node still has relationships +MATCH (n:Person {id: $id}) +DELETE n + +// CORRECT - Delete relationships first +MATCH (n:Person {id: $id}) +DETACH DELETE n +``` + +### Optional Match and Delete + +```cypher +// WRONG - DELETE NULL causes no error but also doesn't help +OPTIONAL MATCH (n:Node {id: $id}) +DELETE n // If n is NULL, nothing happens silently + +// Better - Check existence first or handle in application +MATCH (n:Node {id: $id}) +DELETE n +``` + +## Debugging Tips + +1. **Use EXPLAIN** to see query plan without executing +2. **Use PROFILE** to see actual execution metrics +3. **Break complex queries** into smaller parts to isolate issues +4. **Check parameter types** - mismatched types are a common issue +5. **Verify indexes exist** with `SHOW INDEXES` +6. **Check constraints** with `SHOW CONSTRAINTS` diff --git a/.claude/skills/cypher/references/common-patterns.md b/.claude/skills/cypher/references/common-patterns.md new file mode 100644 index 0000000..5a53ee9 --- /dev/null +++ b/.claude/skills/cypher/references/common-patterns.md @@ -0,0 +1,397 @@ +# Common Cypher Patterns for ORLY Nostr Relay + +This reference contains project-specific Cypher patterns used in the ORLY Nostr relay's Neo4j backend. + +## Schema Overview + +### Node Types + +| Label | Purpose | Key Properties | +|-------|---------|----------------| +| `Event` | Nostr events (NIP-01) | `id`, `kind`, `pubkey`, `created_at`, `content`, `sig`, `tags`, `serial` | +| `Author` | Event authors (for NIP-01 queries) | `pubkey` | +| `Tag` | Generic tags | `type`, `value` | +| `NostrUser` | Social graph users (WoT) | `pubkey`, `name`, `about`, `picture`, `nip05` | +| `ProcessedSocialEvent` | Social event tracking | `event_id`, `event_kind`, `pubkey`, `superseded_by` | +| `Marker` | Internal state markers | `key`, `value` | + +### Relationship Types + +| Type | From | To | Purpose | +|------|------|-----|---------| +| `AUTHORED_BY` | Event | Author | Links event to author | +| `TAGGED_WITH` | Event | Tag | Links event to tags | +| `REFERENCES` | Event | Event | e-tag references | +| `MENTIONS` | Event | Author | p-tag mentions | +| `FOLLOWS` | NostrUser | NostrUser | Contact list (kind 3) | +| `MUTES` | NostrUser | NostrUser | Mute list (kind 10000) | +| `REPORTS` | NostrUser | NostrUser | Reports (kind 1984) | + +## Event Storage Patterns + +### Create Event with Full Relationships + +This pattern creates an event and all related nodes/relationships atomically: + +```cypher +// 1. Create or get author +MERGE (a:Author {pubkey: $pubkey}) + +// 2. Create event node +CREATE (e:Event { + id: $eventId, + serial: $serial, + kind: $kind, + created_at: $createdAt, + content: $content, + sig: $sig, + pubkey: $pubkey, + tags: $tagsJson // JSON string for full tag data +}) + +// 3. Link to author +CREATE (e)-[:AUTHORED_BY]->(a) + +// 4. Process e-tags (event references) +WITH e, a +OPTIONAL MATCH (ref0:Event {id: $eTag_0}) +FOREACH (_ IN CASE WHEN ref0 IS NOT NULL THEN [1] ELSE [] END | + CREATE (e)-[:REFERENCES]->(ref0) +) + +// 5. Process p-tags (mentions) +WITH e, a +MERGE (mentioned0:Author {pubkey: $pTag_0}) +CREATE (e)-[:MENTIONS]->(mentioned0) + +// 6. Process other tags +WITH e, a +MERGE (tag0:Tag {type: $tagType_0, value: $tagValue_0}) +CREATE (e)-[:TAGGED_WITH]->(tag0) + +RETURN e.id AS id +``` + +### Check Event Existence + +```cypher +MATCH (e:Event {id: $id}) +RETURN e.id AS id +LIMIT 1 +``` + +### Get Next Serial Number + +```cypher +MERGE (m:Marker {key: 'serial'}) +ON CREATE SET m.value = 1 +ON MATCH SET m.value = m.value + 1 +RETURN m.value AS serial +``` + +## Query Patterns + +### Basic Filter Query (NIP-01) + +```cypher +MATCH (e:Event) +WHERE e.kind IN $kinds + AND e.pubkey IN $authors + AND e.created_at >= $since + AND e.created_at <= $until +RETURN e.id AS id, + e.kind AS kind, + e.created_at AS created_at, + e.content AS content, + e.sig AS sig, + e.pubkey AS pubkey, + e.tags AS tags, + e.serial AS serial +ORDER BY e.created_at DESC +LIMIT $limit +``` + +### Query by Event ID (with prefix support) + +```cypher +// Exact match +MATCH (e:Event {id: $id}) +RETURN e + +// Prefix match +MATCH (e:Event) +WHERE e.id STARTS WITH $idPrefix +RETURN e +``` + +### Query by Tag (# filter) + +```cypher +MATCH (e:Event) +OPTIONAL MATCH (e)-[:TAGGED_WITH]->(t:Tag) +WHERE t.type = $tagType AND t.value IN $tagValues +RETURN DISTINCT e +ORDER BY e.created_at DESC +LIMIT $limit +``` + +### Count Events + +```cypher +MATCH (e:Event) +WHERE e.kind IN $kinds +RETURN count(e) AS count +``` + +### Query Delete Events Targeting an Event + +```cypher +MATCH (target:Event {id: $targetId}) +MATCH (e:Event {kind: 5})-[:REFERENCES]->(target) +RETURN e +ORDER BY e.created_at DESC +``` + +### Replaceable Event Check (kinds 0, 3, 10000-19999) + +```cypher +MATCH (e:Event {kind: $kind, pubkey: $pubkey}) +WHERE e.created_at < $newCreatedAt +RETURN e.serial AS serial +ORDER BY e.created_at DESC +``` + +### Parameterized Replaceable Event Check (kinds 30000-39999) + +```cypher +MATCH (e:Event {kind: $kind, pubkey: $pubkey})-[:TAGGED_WITH]->(t:Tag {type: 'd', value: $dValue}) +WHERE e.created_at < $newCreatedAt +RETURN e.serial AS serial +ORDER BY e.created_at DESC +``` + +## Social Graph Patterns + +### Update Profile (Kind 0) + +```cypher +MERGE (user:NostrUser {pubkey: $pubkey}) +ON CREATE SET + user.created_at = timestamp(), + user.first_seen_event = $event_id +ON MATCH SET + user.last_profile_update = $created_at +SET + user.name = $name, + user.about = $about, + user.picture = $picture, + user.nip05 = $nip05, + user.lud16 = $lud16, + user.display_name = $display_name +``` + +### Contact List Update (Kind 3) - Diff-Based + +```cypher +// Mark old event as superseded +OPTIONAL MATCH (old:ProcessedSocialEvent {event_id: $old_event_id}) +SET old.superseded_by = $new_event_id + +// Create new event tracking +CREATE (new:ProcessedSocialEvent { + event_id: $new_event_id, + event_kind: 3, + pubkey: $author_pubkey, + created_at: $created_at, + processed_at: timestamp(), + relationship_count: $total_follows, + superseded_by: null +}) + +// Get or create author +MERGE (author:NostrUser {pubkey: $author_pubkey}) + +// Update unchanged relationships to new event +WITH author +OPTIONAL MATCH (author)-[unchanged:FOLLOWS]->(followed:NostrUser) +WHERE unchanged.created_by_event = $old_event_id + AND NOT followed.pubkey IN $removed_follows +SET unchanged.created_by_event = $new_event_id, + unchanged.created_at = $created_at + +// Remove old relationships for removed follows +WITH author +OPTIONAL MATCH (author)-[old_follows:FOLLOWS]->(followed:NostrUser) +WHERE old_follows.created_by_event = $old_event_id + AND followed.pubkey IN $removed_follows +DELETE old_follows + +// Create new relationships for added follows +WITH author +UNWIND $added_follows AS followed_pubkey +MERGE (followed:NostrUser {pubkey: followed_pubkey}) +MERGE (author)-[new_follows:FOLLOWS]->(followed) +ON CREATE SET + new_follows.created_by_event = $new_event_id, + new_follows.created_at = $created_at, + new_follows.relay_received_at = timestamp() +ON MATCH SET + new_follows.created_by_event = $new_event_id, + new_follows.created_at = $created_at +``` + +### Create Report (Kind 1984) + +```cypher +// Create tracking node +CREATE (evt:ProcessedSocialEvent { + event_id: $event_id, + event_kind: 1984, + pubkey: $reporter_pubkey, + created_at: $created_at, + processed_at: timestamp(), + relationship_count: 1, + superseded_by: null +}) + +// Create users and relationship +MERGE (reporter:NostrUser {pubkey: $reporter_pubkey}) +MERGE (reported:NostrUser {pubkey: $reported_pubkey}) +CREATE (reporter)-[:REPORTS { + created_by_event: $event_id, + created_at: $created_at, + relay_received_at: timestamp(), + report_type: $report_type +}]->(reported) +``` + +### Get Latest Social Event for Pubkey + +```cypher +MATCH (evt:ProcessedSocialEvent {pubkey: $pubkey, event_kind: $kind}) +WHERE evt.superseded_by IS NULL +RETURN evt.event_id AS event_id, + evt.created_at AS created_at, + evt.relationship_count AS relationship_count +ORDER BY evt.created_at DESC +LIMIT 1 +``` + +### Get Follows for Event + +```cypher +MATCH (author:NostrUser)-[f:FOLLOWS]->(followed:NostrUser) +WHERE f.created_by_event = $event_id +RETURN collect(followed.pubkey) AS pubkeys +``` + +## WoT Query Patterns + +### Find Mutual Follows + +```cypher +MATCH (a:NostrUser {pubkey: $pubkeyA})-[:FOLLOWS]->(b:NostrUser) +WHERE (b)-[:FOLLOWS]->(a) +RETURN b.pubkey AS mutual_friend +``` + +### Find Followers + +```cypher +MATCH (follower:NostrUser)-[:FOLLOWS]->(user:NostrUser {pubkey: $pubkey}) +RETURN follower.pubkey, follower.name +``` + +### Find Following + +```cypher +MATCH (user:NostrUser {pubkey: $pubkey})-[:FOLLOWS]->(following:NostrUser) +RETURN following.pubkey, following.name +``` + +### Hop Distance (Trust Path) + +```cypher +MATCH (start:NostrUser {pubkey: $startPubkey}) +MATCH (end:NostrUser {pubkey: $endPubkey}) +MATCH path = shortestPath((start)-[:FOLLOWS*..6]->(end)) +RETURN length(path) AS hops, [n IN nodes(path) | n.pubkey] AS path +``` + +### Second-Degree Connections + +```cypher +MATCH (me:NostrUser {pubkey: $myPubkey})-[:FOLLOWS]->(:NostrUser)-[:FOLLOWS]->(suggested:NostrUser) +WHERE NOT (me)-[:FOLLOWS]->(suggested) + AND suggested.pubkey <> $myPubkey +RETURN suggested.pubkey, count(*) AS commonFollows +ORDER BY commonFollows DESC +LIMIT 20 +``` + +## Schema Management Patterns + +### Create Constraint + +```cypher +CREATE CONSTRAINT event_id_unique IF NOT EXISTS +FOR (e:Event) REQUIRE e.id IS UNIQUE +``` + +### Create Index + +```cypher +CREATE INDEX event_kind IF NOT EXISTS +FOR (e:Event) ON (e.kind) +``` + +### Create Composite Index + +```cypher +CREATE INDEX event_kind_created_at IF NOT EXISTS +FOR (e:Event) ON (e.kind, e.created_at) +``` + +### Drop All Data (Testing Only) + +```cypher +MATCH (n) DETACH DELETE n +``` + +## Performance Patterns + +### Use EXPLAIN/PROFILE + +```cypher +// See query plan without running +EXPLAIN MATCH (e:Event) WHERE e.kind = 1 RETURN e + +// Run and see actual metrics +PROFILE MATCH (e:Event) WHERE e.kind = 1 RETURN e +``` + +### Batch Import with UNWIND + +```cypher +UNWIND $events AS evt +CREATE (e:Event { + id: evt.id, + kind: evt.kind, + pubkey: evt.pubkey, + created_at: evt.created_at, + content: evt.content, + sig: evt.sig, + tags: evt.tags +}) +``` + +### Efficient Pagination + +```cypher +// Use indexed ORDER BY with WHERE for cursor-based pagination +MATCH (e:Event) +WHERE e.kind = 1 AND e.created_at < $cursor +RETURN e +ORDER BY e.created_at DESC +LIMIT 20 +``` diff --git a/.claude/skills/cypher/references/syntax-reference.md b/.claude/skills/cypher/references/syntax-reference.md new file mode 100644 index 0000000..ebb94b2 --- /dev/null +++ b/.claude/skills/cypher/references/syntax-reference.md @@ -0,0 +1,540 @@ +# Cypher Syntax Reference + +Complete syntax reference for Neo4j Cypher query language. + +## Clause Reference + +### Reading Clauses + +#### MATCH + +Finds patterns in the graph. + +```cypher +// Basic node match +MATCH (n:Label) + +// Match with properties +MATCH (n:Label {key: value}) + +// Match relationships +MATCH (a)-[r:RELATES_TO]->(b) + +// Match path +MATCH path = (a)-[*1..3]->(b) +``` + +#### OPTIONAL MATCH + +Like MATCH but returns NULL for non-matches (LEFT OUTER JOIN). + +```cypher +MATCH (a:Person) +OPTIONAL MATCH (a)-[:KNOWS]->(b:Person) +RETURN a.name, b.name // b.name may be NULL +``` + +#### WHERE + +Filters results. + +```cypher +// Comparison operators +WHERE n.age > 21 +WHERE n.age >= 21 +WHERE n.age < 65 +WHERE n.age <= 65 +WHERE n.name = 'Alice' +WHERE n.name <> 'Bob' + +// Boolean operators +WHERE n.age > 21 AND n.active = true +WHERE n.age < 18 OR n.age > 65 +WHERE NOT n.deleted + +// NULL checks +WHERE n.email IS NULL +WHERE n.email IS NOT NULL + +// Pattern predicates +WHERE (n)-[:KNOWS]->(:Person) +WHERE NOT (n)-[:BLOCKED]->() +WHERE exists((n)-[:FOLLOWS]->()) + +// String predicates +WHERE n.name STARTS WITH 'A' +WHERE n.name ENDS WITH 'son' +WHERE n.name CONTAINS 'li' +WHERE n.name =~ '(?i)alice.*' // Case-insensitive regex + +// List predicates +WHERE n.status IN ['active', 'pending'] +WHERE any(x IN n.tags WHERE x = 'important') +WHERE all(x IN n.scores WHERE x > 50) +WHERE none(x IN n.errors WHERE x IS NOT NULL) +WHERE single(x IN n.items WHERE x.primary = true) +``` + +### Writing Clauses + +#### CREATE + +Creates nodes and relationships. + +```cypher +// Create node +CREATE (n:Label {key: value}) + +// Create multiple nodes +CREATE (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) + +// Create relationship +CREATE (a)-[r:KNOWS {since: 2020}]->(b) + +// Create path +CREATE p = (a)-[:KNOWS]->(b)-[:KNOWS]->(c) +``` + +#### MERGE + +Find or create pattern. **Critical for idempotency**. + +```cypher +// MERGE node +MERGE (n:Label {key: $uniqueKey}) + +// MERGE with ON CREATE / ON MATCH +MERGE (n:Person {email: $email}) +ON CREATE SET n.created = timestamp(), n.name = $name +ON MATCH SET n.accessed = timestamp() + +// MERGE relationship (both nodes must exist or be in scope) +MERGE (a)-[r:KNOWS]->(b) +ON CREATE SET r.since = date() +``` + +**MERGE Gotcha**: MERGE on a pattern locks the entire pattern. For relationships, MERGE each node first: + +```cypher +// CORRECT +MERGE (a:Person {id: $id1}) +MERGE (b:Person {id: $id2}) +MERGE (a)-[:KNOWS]->(b) + +// RISKY - may create duplicate nodes +MERGE (a:Person {id: $id1})-[:KNOWS]->(b:Person {id: $id2}) +``` + +#### SET + +Updates properties. + +```cypher +// Set single property +SET n.name = 'Alice' + +// Set multiple properties +SET n.name = 'Alice', n.age = 30 + +// Set from map (replaces all properties) +SET n = {name: 'Alice', age: 30} + +// Set from map (adds/updates, keeps existing) +SET n += {name: 'Alice'} + +// Set label +SET n:NewLabel + +// Remove property +SET n.obsolete = null +``` + +#### DELETE / DETACH DELETE + +Removes nodes and relationships. + +```cypher +// Delete relationship +MATCH (a)-[r:KNOWS]->(b) +DELETE r + +// Delete node (must have no relationships) +MATCH (n:Orphan) +DELETE n + +// Delete node and all relationships +MATCH (n:Person {name: 'Bob'}) +DETACH DELETE n +``` + +#### REMOVE + +Removes properties and labels. + +```cypher +// Remove property +REMOVE n.temporary + +// Remove label +REMOVE n:OldLabel +``` + +### Projection Clauses + +#### RETURN + +Specifies output. + +```cypher +// Return nodes +RETURN n + +// Return properties +RETURN n.name, n.age + +// Return with alias +RETURN n.name AS name, n.age AS age + +// Return all +RETURN * + +// Return distinct +RETURN DISTINCT n.category + +// Return expression +RETURN n.price * n.quantity AS total +``` + +#### WITH + +Passes results between query parts. **Critical for multi-part queries**. + +```cypher +// Filter and pass +MATCH (n:Person) +WITH n WHERE n.age > 21 +RETURN n + +// Aggregate and continue +MATCH (n:Person)-[:BOUGHT]->(p:Product) +WITH n, count(p) AS purchases +WHERE purchases > 5 +RETURN n.name, purchases + +// Order and limit mid-query +MATCH (n:Person) +WITH n ORDER BY n.age DESC LIMIT 10 +MATCH (n)-[:LIVES_IN]->(c:City) +RETURN n.name, c.name +``` + +**WITH resets scope**: Variables not listed in WITH are no longer available. + +#### ORDER BY + +Sorts results. + +```cypher +ORDER BY n.name // Ascending (default) +ORDER BY n.name ASC // Explicit ascending +ORDER BY n.name DESC // Descending +ORDER BY n.lastName, n.firstName // Multiple fields +ORDER BY n.priority DESC, n.name // Mixed +``` + +#### SKIP and LIMIT + +Pagination. + +```cypher +// Skip first 10 +SKIP 10 + +// Return only 20 +LIMIT 20 + +// Pagination +ORDER BY n.created_at DESC +SKIP $offset LIMIT $pageSize +``` + +### Sub-queries + +#### CALL (Subquery) + +Execute subquery for each row. + +```cypher +MATCH (p:Person) +CALL { + WITH p + MATCH (p)-[:BOUGHT]->(prod:Product) + RETURN count(prod) AS purchaseCount +} +RETURN p.name, purchaseCount +``` + +#### UNION + +Combine results from multiple queries. + +```cypher +MATCH (n:Person) RETURN n.name AS name +UNION +MATCH (n:Company) RETURN n.name AS name + +// UNION ALL keeps duplicates +MATCH (n:Person) RETURN n.name AS name +UNION ALL +MATCH (n:Company) RETURN n.name AS name +``` + +### Control Flow + +#### FOREACH + +Iterate over list, execute updates. + +```cypher +// Set property on path nodes +MATCH path = (a)-[*]->(b) +FOREACH (n IN nodes(path) | SET n.visited = true) + +// Conditional operation (common pattern) +OPTIONAL MATCH (target:Node {id: $id}) +FOREACH (_ IN CASE WHEN target IS NOT NULL THEN [1] ELSE [] END | + CREATE (source)-[:LINKS_TO]->(target) +) +``` + +#### CASE + +Conditional expressions. + +```cypher +// Simple CASE +RETURN CASE n.status + WHEN 'active' THEN 'A' + WHEN 'pending' THEN 'P' + ELSE 'X' +END AS code + +// Generic CASE +RETURN CASE + WHEN n.age < 18 THEN 'minor' + WHEN n.age < 65 THEN 'adult' + ELSE 'senior' +END AS category +``` + +## Operators + +### Comparison + +| Operator | Description | +|----------|-------------| +| `=` | Equal | +| `<>` | Not equal | +| `<` | Less than | +| `>` | Greater than | +| `<=` | Less than or equal | +| `>=` | Greater than or equal | +| `IS NULL` | Is null | +| `IS NOT NULL` | Is not null | + +### Boolean + +| Operator | Description | +|----------|-------------| +| `AND` | Logical AND | +| `OR` | Logical OR | +| `NOT` | Logical NOT | +| `XOR` | Exclusive OR | + +### String + +| Operator | Description | +|----------|-------------| +| `STARTS WITH` | Prefix match | +| `ENDS WITH` | Suffix match | +| `CONTAINS` | Substring match | +| `=~` | Regex match | + +### List + +| Operator | Description | +|----------|-------------| +| `IN` | List membership | +| `+` | List concatenation | + +### Mathematical + +| Operator | Description | +|----------|-------------| +| `+` | Addition | +| `-` | Subtraction | +| `*` | Multiplication | +| `/` | Division | +| `%` | Modulo | +| `^` | Exponentiation | + +## Functions + +### Aggregation + +```cypher +count(*) // Count rows +count(n) // Count non-null +count(DISTINCT n) // Count unique +sum(n.value) // Sum +avg(n.value) // Average +min(n.value) // Minimum +max(n.value) // Maximum +collect(n) // Collect to list +collect(DISTINCT n) // Collect unique +stDev(n.value) // Standard deviation +percentileCont(n.value, 0.5) // Median +``` + +### Scalar + +```cypher +// Type functions +id(n) // Internal node ID (deprecated, use elementId) +elementId(n) // Element ID string +labels(n) // Node labels +type(r) // Relationship type +properties(n) // Property map + +// Math +abs(x) +ceil(x) +floor(x) +round(x) +sign(x) +sqrt(x) +rand() // Random 0-1 + +// String +size(str) // String length +toLower(str) +toUpper(str) +trim(str) +ltrim(str) +rtrim(str) +replace(str, from, to) +substring(str, start, len) +left(str, len) +right(str, len) +split(str, delimiter) +reverse(str) +toString(val) + +// Null handling +coalesce(val1, val2, ...) // First non-null +nullIf(val1, val2) // NULL if equal + +// Type conversion +toInteger(val) +toFloat(val) +toBoolean(val) +toString(val) +``` + +### List Functions + +```cypher +size(list) // List length +head(list) // First element +tail(list) // All but first +last(list) // Last element +range(start, end) // Create range [start..end] +range(start, end, step) +reverse(list) +keys(map) // Map keys as list +values(map) // Map values as list + +// List predicates +any(x IN list WHERE predicate) +all(x IN list WHERE predicate) +none(x IN list WHERE predicate) +single(x IN list WHERE predicate) + +// List manipulation +[x IN list WHERE predicate] // Filter +[x IN list | expression] // Map +[x IN list WHERE pred | expr] // Filter and map +reduce(s = initial, x IN list | s + x) // Reduce +``` + +### Path Functions + +```cypher +nodes(path) // Nodes in path +relationships(path) // Relationships in path +length(path) // Number of relationships +shortestPath((a)-[*]-(b)) +allShortestPaths((a)-[*]-(b)) +``` + +### Temporal Functions + +```cypher +timestamp() // Current Unix timestamp (ms) +datetime() // Current datetime +date() // Current date +time() // Current time +duration({days: 1, hours: 12}) + +// Components +datetime().year +datetime().month +datetime().day +datetime().hour + +// Parsing +date('2024-01-15') +datetime('2024-01-15T10:30:00Z') +``` + +### Spatial Functions + +```cypher +point({x: 1, y: 2}) +point({latitude: 37.5, longitude: -122.4}) +distance(point1, point2) +``` + +## Comments + +```cypher +// Single line comment + +/* Multi-line + comment */ +``` + +## Transaction Control + +```cypher +// In procedures/transactions +:begin +:commit +:rollback +``` + +## Parameter Syntax + +```cypher +// Parameter reference +$paramName + +// In properties +{key: $value} + +// In WHERE +WHERE n.id = $id + +// In expressions +RETURN $multiplier * n.value +```