-
Notifications
You must be signed in to change notification settings - Fork 55
chore: add neo4j cypher, python driver, and query tuning skills #9967
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dgarros
wants to merge
4
commits into
stable
Choose a base branch
from
dga/chore-neo4j-skill-i3e03
base: stable
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
f00fe93
chore: add neo4j cypher, python driver, and query tuning skills
dgarros 6361dd0
ci: exclude vendored agent-skill scripts from ruff
dgarros aa0134d
ci: exclude vendored agent-skill scripts from ty type check
dgarros 46714f7
ci: add cosign's to vale spelling exceptions
dgarros File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # neo4j-cypher-skill | ||
|
|
||
| Generates, optimizes, and validates Cypher 25 queries for Neo4j 2025.x and 2026.x. | ||
|
|
||
| ## Topics covered | ||
|
|
||
| **Query writing** — reads, writes, subqueries, batch operations, LOAD CSV, schema inspection, EXPLAIN/PROFILE validation | ||
|
|
||
| **Patterns** — MATCH, OPTIONAL MATCH, WITH, UNION, MERGE (constrained-key rules), FOREACH, UNWIND, CALL IN TRANSACTIONS | ||
|
|
||
| **Subqueries** — `EXISTS {}`, `COUNT {}`, `COLLECT {}`, `CALL (x) { }`, `OPTIONAL CALL` | ||
|
|
||
| **Path expressions** — Quantified Path Expressions (QPEs), match modes (`DIFFERENT RELATIONSHIPS`, `REPEATABLE ELEMENTS`), path selectors (`SHORTEST 1`, `ALL SHORTEST`) | ||
|
|
||
| **Search** — vector search (`SEARCH` clause 2026.01+, procedure fallback for 2025.x), fulltext (`db.index.fulltext`) | ||
|
|
||
| **Schema** — `db.schema.visualization`, `SHOW INDEXES/CONSTRAINTS/PROCEDURES`, `apoc.meta.schema()` (preferred when APOC available) | ||
|
|
||
| **Performance** — Eager operator detection and fixes, parallel runtime, index hints, anti-patterns by severity | ||
|
|
||
| **Language features** — dynamic labels/properties (2025.01), type predicates (`IS :: INTEGER NOT NULL`), `OrNull` casting, `coll.sort()`, `btrim()`, date/time arithmetic, null handling, 40+ syntax traps | ||
|
|
||
| ## Version coverage | ||
|
|
||
| Defaults to 2025.01-safe features. Items new in 2025.x are annotated `[2025.01]` in the reference files; 2026.x items `[2026.01]`. | ||
|
|
||
| ## Not covered | ||
|
|
||
| - Driver migration → `neo4j-migration-skill` | ||
| - DB administration → `neo4j-cli-tools-skill` | ||
|
|
||
| ## Reference files | ||
|
|
||
| Loaded on demand — not bundled into the main skill context: | ||
|
|
||
| | File | Contents | | ||
| |---|---| | ||
| | [`references/cypher-syntax.md`](references/cypher-syntax.md) | Full syntax reference: clauses, patterns, functions. Items introduced in 2025.x annotated `[2025.01]`; 2026.x items `[2026.01]`; older deprecated forms annotated `[replaces X]` | | ||
| | [`references/syntax-traps.md`](references/syntax-traps.md) | 40+ table of invalid → correct Cypher — SQL habits and pre-2025 syntax | | ||
| | [`references/performance.md`](references/performance.md) | Anti-patterns with severity levels, text vs fulltext index comparison, Eager operator triggers and fixes | | ||
|
|
||
| ## Not covered | ||
|
|
||
| - Driver migration or version upgrade → `neo4j-migration-skill` | ||
| - Database administration (users, config, backups) → `neo4j-cli-tools-skill` | ||
| - GQL clauses: `LET`, `FINISH`, `FILTER`, and `INSERT` are valid in Cypher 25 (introduced via GQL conformance, mostly in Neo4j 2025.06); not available on older versions | ||
|
|
||
| ## Related skills | ||
|
|
||
| | Skill | Purpose | | ||
| |---|---| | ||
| | `neo4j-getting-started-skill` | Zero-to-app: provision, model, load, explore, build | | ||
| | `neo4j-migration-skill` | Upgrade Cypher syntax and drivers across major versions | | ||
| | `neo4j-cli-tools-skill` | DB administration via `neo4j-admin`, `cypher-shell`, Aura CLI | | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| npx skills add https://github.com/neo4j-contrib/neo4j-skills --skill neo4j-cypher-skill | ||
| ``` | ||
|
Comment on lines
+48
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can probably dump this. might be similar stuff in the other skills |
||
Large diffs are not rendered by default.
Oops, something went wrong.
243 changes: 243 additions & 0 deletions
243
.agents/skills/neo4j-cypher-skill/references/advanced-patterns.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,243 @@ | ||
| # Advanced Graph Patterns | ||
|
|
||
| Load when solving path-finding, fraud detection, DAG traversal, temporal graphs, or stateful QPE problems. | ||
|
|
||
| Version markers: `[Neo4j 5]` = Neo4j 5.x, `[2025.x]` = Neo4j 2025.x / Cypher 25, `[2025.06]` = 2025.06+. | ||
|
|
||
| --- | ||
|
|
||
| ## REPEATABLE ELEMENTS — When to Use [2025.x] | ||
|
|
||
| Default: `DIFFERENT RELATIONSHIPS` — each relationship traversed at most once per path. | ||
| Use `REPEATABLE ELEMENTS` when: | ||
| - Nodes have limited connectivity (single in/out relationship) and weight-optimized paths are needed | ||
| - Problem requires backtracking through already-visited nodes (circular routes, constrained path search) | ||
| - Path must revisit waypoints (multi-stop routes, recurring visits) | ||
|
|
||
| ```cypher | ||
| // Find circular routes from CPH using same connections multiple times | ||
| CYPHER 25 | ||
| MATCH (cph:Airport {iata: 'CPH'}) | ||
| MATCH REPEATABLE ELEMENTS p=(cph)(()-[c:CONNECTION]-() WHERE c.km < 548){2,6}(cph) | ||
| WITH p, reduce(d=0, c IN relationships(p) | d + c.km) AS distance | ||
| WHERE distance >= 805 | ||
| ORDER BY distance LIMIT 1 | ||
| RETURN p, distance | ||
| ``` | ||
|
|
||
| `REPEATABLE ELEMENTS` requires bounded quantifier `{m,n}` — do not use `{1,}` (unbounded). | ||
|
|
||
| --- | ||
|
|
||
| ## Multi-Stop Shortest Path [2025.x] | ||
|
|
||
| Multiple waypoints in one query via chained QPE groups: | ||
|
|
||
| ```cypher | ||
| CYPHER 25 | ||
| MATCH REPEATABLE ELEMENTS p = | ||
| ALL SHORTEST (:Airport {iata:'CPH'})--{,10} | ||
| (:Airport {iata:'IFJ'})--{,10} | ||
| (:Airport {iata:'DFW'}) | ||
| WITH p, reduce(d=0, c IN relationships(p) | d + c.km) AS distance | ||
| ORDER BY distance LIMIT 1 | ||
| RETURN p, distance | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Stateful Route Planning with allReduce [2025.x] | ||
|
|
||
| `allReduce` for simulation-style traversal where path validity depends on accumulated state (energy, time, cost): | ||
|
|
||
| ```cypher | ||
| CYPHER 25 runtime=parallel | ||
| MATCH (src:Geo {name: $source}), (dst:Geo {name: $target}) | ||
| MATCH REPEATABLE ELEMENTS p=(src)(()-[r:ROAD|CHARGE]-(x:Geo)){1,12}(dst) | ||
| WHERE allReduce( | ||
| curr = {soc: $initial_soc_pct, mins: 0.0}, | ||
| r IN relationships(p) | | ||
| CASE | ||
| WHEN r:ROAD THEN {soc: curr.soc - r.drain_pct, mins: curr.mins + r.drive_mins} | ||
| WHEN r:CHARGE THEN {soc: curr.soc + r.charge_pct, mins: curr.mins + r.charge_mins} | ||
| END, | ||
| $min_soc <= curr.soc <= $max_soc AND curr.mins <= $max_mins | ||
| ) | ||
| // Spatial pre-filter: skip detours > 1.3x direct distance | ||
| AND ALL(x IN nodes(p) WHERE | ||
| point.distance(x.geo, dst.geo) < 1.3 * point.distance(src.geo, dst.geo)) | ||
| RETURN p, reduce(d=0, r IN relationships(p) | d + r.drive_mins) AS total_mins | ||
| ORDER BY total_mins ASC LIMIT 1 | ||
| ``` | ||
|
|
||
| `allReduce(accumulator = initial, item IN list | updateExpr, predicate)` — returns `true` only if predicate holds at every step. Prunes invalid paths inline during expansion. | ||
|
|
||
| --- | ||
|
|
||
| ## DAG Traversal and Critical Path [Neo4j 5] | ||
|
|
||
| Model: ActivityStart/ActivityEnd nodes connected by weighted `:ACTIVITY` edges; zero-weight `:DEPENDS_ON` edges for sequencing. | ||
|
|
||
| ```cypher | ||
| // Longest path (critical path) — small graphs only | ||
| // For large graphs use gds.dag.longestPath instead | ||
| CYPHER 25 | ||
| MATCH p=(a:ActivityStart)-[:ACTIVITY|DEPENDS_ON]*->(b:ActivityEnd) | ||
| WITH b.name AS task, | ||
| reduce(t=0, r IN [x IN relationships(p) WHERE type(x)='ACTIVITY' | x] | | ||
| t + r.expectedTime) AS totalTime | ||
| RETURN task, max(totalTime) AS criticalPathTime | ||
| ORDER BY criticalPathTime DESC | ||
|
|
||
| // GDS alternative for large DAGs (much faster): | ||
| // CALL gds.dag.longestPath.stream('dag_graph') YIELD nodeId, distance | ||
| ``` | ||
|
|
||
| Limitation: Cypher QPE for longest path fails on large graphs; use `gds.dag.longestPath` for production. | ||
|
|
||
| --- | ||
|
|
||
| ## Fraud Detection — Temporal Component Graph [2025.x] | ||
|
|
||
| Pattern: User-Event-Thing model where fraud rings = connected components sharing resources (IPs, devices, emails). | ||
|
|
||
| **Problem**: Standard WCC includes future events → "future leakage" in ML features. | ||
| **Solution**: Chronological `:SAME_CC_AS` forest — each event links only to components existing at its timestamp. | ||
|
|
||
| ```cypher | ||
| // Step 1: Build temporal connected components (process events in timestamp order) | ||
| CYPHER 25 | ||
| MATCH (e:Event&!ConnectedComponent) | ||
| WITH e ORDER BY e.timestamp | ||
| CALL (e) { | ||
| MATCH (e)(()-[:WITH]->(entity)<-[:WITH]-(:ConnectedComponent)){0,1}()<-[:COMMITS]-(u) | ||
| WITH DISTINCT e, u | ||
| MATCH (u)-[:SAME_CC_AS]->*(cc WHERE NOT EXISTS {(cc)-[:SAME_CC_AS]->()}) | ||
| MERGE (cc)-[:SAME_CC_AS]->(e) | ||
| SET e:ConnectedComponent | ||
| } IN TRANSACTIONS OF 100 ROWS | ||
|
|
||
| // Step 2: Point-in-time component snapshot (features as of $asOfDate) | ||
| CYPHER 25 | ||
| MATCH (cc:Event) | ||
| WHERE cc.timestamp <= $asOfDate | ||
| AND NOT EXISTS {(cc)-[:SAME_CC_AS]->(x:Event WHERE x.timestamp <= $asOfDate)} | ||
| RETURN cc | ||
|
|
||
| // Step 3: Retrieve component membership for an event | ||
| CYPHER 25 | ||
| MATCH p=(u:User)(()-[:SAME_CC_AS]->(ev))*(e:Event {event_id: $event_id}) | ||
| UNWIND ev + [e] AS event | ||
| RETURN p, [(event)-[r:WITH]->(x) | [r, x]] AS with_things | ||
| ``` | ||
|
|
||
| **Scaling with GDS WCC** — process independent components in parallel: | ||
| ```cypher | ||
| CYPHER 25 | ||
| CALL gds.wcc.stream('wcc_graph') YIELD nodeId, componentId | ||
| WITH gds.util.asNode(nodeId) AS event, componentId | ||
| WITH componentId, collect(event) AS events | ||
| ORDER BY rand() | ||
| CALL (events) { | ||
| UNWIND events AS e | ||
| WHERE NOT e:ConnectedComponent | ||
| ORDER BY e.timestamp ASC | ||
| CALL (e) { | ||
| MATCH (e)(()-[:WITH]->(entity)<-[:WITH]-(:ConnectedComponent)){0,1}()<-[:COMMITS]-(p) | ||
| WITH DISTINCT e, p | ||
| MATCH (p)-[:SAME_CC_AS]->*(cc WHERE NOT EXISTS {(cc)-[:SAME_CC_AS]->()}) | ||
| MERGE (cc)-[:SAME_CC_AS]->(e) | ||
| SET e:ConnectedComponent | ||
| } | ||
| } IN CONCURRENT TRANSACTIONS OF 100 ROWS | ||
| ``` | ||
|
|
||
| **Avoid O(n²) clique projection** — use linear path through shared entity instead: | ||
| ```cypher | ||
| CYPHER 25 | ||
| MATCH (thing:Thing|User) | ||
| CALL (thing) { | ||
| MATCH (e:Event)-[:WITH|COMMITS]-(thing) | ||
| WITH DISTINCT e | ||
| WITH collect(e) AS events | ||
| WITH CASE size(events) WHEN 1 THEN [events[0], null] ELSE events END AS events | ||
| UNWIND range(0, size(events)-2) AS ix | ||
| RETURN events[ix] AS source, events[ix+1] AS target | ||
| } | ||
| RETURN gds.graph.project('wcc_graph', source, target, {}) | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Cycle Detection with QPE [Neo4j 5] | ||
|
|
||
| Detect non-repeating cycles without artificial length limits: | ||
|
|
||
| ```cypher | ||
| // All cycles through a node (bounded for safety) | ||
| CYPHER 25 | ||
| MATCH (start:Account {id: $id}) | ||
| MATCH DIFFERENT RELATIONSHIPS p=(start)(()-[:TRANSFERS_TO]->()){2,10}(start) | ||
| RETURN p, length(p) AS cycleLength | ||
| ORDER BY cycleLength LIMIT 20 | ||
|
|
||
| // Count paths through complex small-graph traversal | ||
| CYPHER 25 runtime=parallel | ||
| MATCH REPEATABLE ELEMENTS path=(:Start)((xs:!End)--(:!Start)){0,100}(e:End) | ||
| WHERE allReduce( | ||
| visited = [], | ||
| x IN xs | CASE WHEN x:Big THEN visited ELSE visited + [x] END, | ||
| size(visited) <= size(apoc.coll.toSet(visited)) + 1 | ||
| ) | ||
| RETURN count(path) AS validPaths | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Path Selector Reference [2025.x] | ||
|
|
||
| | Selector | Returns | Use case | | ||
| |---|---|---| | ||
| | `SHORTEST 1` | One shortest path | Existence + distance | | ||
| | `ALL SHORTEST` | All equal-minimum-length paths | Parallel routing | | ||
| | `ANY` | Any path (no length guarantee) | Fast existence check | | ||
| | `SHORTEST k` | k shortest paths | Top-k routing | | ||
| | `SHORTEST k GROUPS` | All paths grouped by length up to k distinct lengths | Tier-based routing | | ||
|
|
||
| ```cypher | ||
| // k-shortest paths with cost | ||
| CYPHER 25 | ||
| MATCH SHORTEST 3 (a:City {name: $from})(()-[r:ROAD]->()){1,}(b:City {name: $to}) | ||
| WITH *, reduce(c=0, r IN relationships(*) | c + r.cost) AS totalCost | ||
| ORDER BY totalCost | ||
| RETURN totalCost, [n IN nodes(*) | n.name] AS route | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Type Predicate for Schema Discovery [Neo4j 5] | ||
|
|
||
| Identifies properties by runtime type — useful in GraphRAG pipelines to auto-detect text fields: | ||
|
|
||
| ```cypher | ||
| // Find all STRING properties on nodes in a label | ||
| CYPHER 25 | ||
| MATCH (n:Article) | ||
| WITH keys(n) AS props, n LIMIT 1 | ||
| UNWIND props AS p | ||
| WHERE n[p] IS :: STRING NOT NULL | ||
| RETURN p AS textProperty | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## OPTIONAL CALL [Neo4j 5] | ||
|
|
||
| Left-outer join for procedures — row kept even if procedure returns no results: | ||
|
|
||
| ```cypher | ||
| CYPHER 25 | ||
| MATCH (m:Movie) | ||
| OPTIONAL CALL apoc.algo.dijkstra(m, $target, 'ROAD', 'distance') YIELD path, weight | ||
| RETURN m.title, weight // weight is null when no path found | ||
| ``` |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The
## Not coveredheading appears twice (lines ~21 and ~42) with overlapping content. The second occurrence lists GQL clauses under the heading## Not coveredbut the GQL note (LET,FINISH,FILTER,INSERTare valid Cypher 25) is actually information about what is covered rather than what isn't — it describes a version-gated capability of the skill. The duplicated heading makes the document harder to skim and the GQL clause note under "Not covered" is semantically misplaced.Prompt for AI agents