Academy
Graph Query with AdaGQL experimental
Graph Query with AdaGQL Querying a graph as a graph: nodes, edges and paths, pattern matching and traversal on the CCI GraphDB with AdaGQL. Property graphs. Property graphs. Identify nodes, edges and properties and model them as a property graph.. Nodes and edges, Properties, Labels, Graph vs relational Nodes and edges A property graph is built from two first-class citizens: nodes (entities, sometimes called vertices) and edges (relationships between two nodes). Unlike a relational schema, where a relationship is usually a foreign key sitting quietly inside a row, an edge in a property graph is a real, addressable object. It has an identity, a direction, and, critically, it can carry its own data. (:Analyst)-[:REVIEWED]->(:Incident) is not a join condition; it is a fact stored once, traversed cheaply, and never recomputed. Properties Both nodes and edges hold properties: simple key-value attributes such as name, since, or confidence. A node models the noun (Analyst {name: "J. Okafor", tier: 2}); an edge models the verb and can itself be qualified (REVIEWED {at: "2026-06-30", verdict: "escalated"}). Putting a timestamp or weight on the edge rather than forcing it into a join row is one of the defining ergonomic wins of the model: it lets a path query filter or aggregate on relationship metadata directly, without a bridge table. Labels Labels classify nodes and edge types classify edges. A node can carry one or more labels (:Host:Asset), letting the same underlying entity participate in multiple type hierarchies without a supertype/subtype table cascade. Labels are also the primary unit of indexing and query planning in the CCI GraphDB: MATCH (h:Host) prunes the search space before a single edge is walked, which is why a query planner checks labels before it ever consults an edge's properties. Choosing labels well, neither too generic (:Thing) nor too narrow (:HostSeenOnTuesday), is the modeling skill this module builds toward. Graph vs relational The same domain can always be forced into third normal form, and for aggregate-heavy, set-oriented questions (totals, group-bys, single-hop lookups) the relational engine remains the right tool: its optimizer, statistics, and decades of join-ordering heuristics are hard to beat. The property graph earns its keep when the *question itself is about connectivity*: how are these two accounts related, is there a path at all, how many hops separate them, which relationship types matter. Expressing that in Structured Query Language (SQL) means guessing the maximum path length up front and writing a self-join per hop, or reaching for a recursive Common Table Expression (CTE) that most engines execute row-by-row. AdaGQL expresses the same question as a single pattern, and the storage engine walks adjacency lists directly rather than re-joining tables. The trade-off is real: graphs are usually worse at large-scale aggregate scans across an entire dataset. This module is about recognizing which shape your question has before you pick your engine. In practice this is rarely an all-or-nothing choice at the system level: many production deployments keep the system of record relational, for its transactional guarantees and mature tooling, and materialize a property-graph view of a subset of that data specifically for the connectivity questions, refreshing it on a schedule or via change-data-capture. Recognizing that the two models can coexist, each answering the class of question it is actually good at, avoids the false debate of picking one engine to replace the other outright. Related CCI capabilities Modeling entities and relationships as nodes, edges and properties is exactly the structure CCI's own incident and intelligence work depends on: an analyst, an incident, and the host it affects are naturally nodes connected by typed edges, not rows joined on foreign keys. CCI's Global Cyber Incident Database (GCIDB) 1834 (https://www.cambridgecyberinternational.com/en/products/gcidb-1834/) curates precisely this kind of relationship data, and the property-graph modeling skills this module builds transfer directly to reasoning about how incidents, actors, and targets connect. Pattern matching. Pattern matching. Identify graph pattern components and write AdaGQL queries that match them.. Patterns, Variable-length paths, Filters, Aggregation over paths Patterns AdaGQL queries read like a drawing of the graph you want back. MATCH (a:Analyst)-[:ESCALATED]->(i:Incident)-[:AFFECTS]->(h:Host) RETURN a.name, h.hostname describes exactly the shape being searched for: an Analyst node, connected by an ESCALATED edge to an Incident node, connected by an AFFECTS edge to a Host node. The engine finds every subgraph matching that shape. This is the core habit shift from Structured Query Language (SQL): instead of declaring which tables to join and on what keys, you sketch the topology directly, and variable names (a, i, h) bind to whichever nodes and edges satisfy it. Variable-length paths Many real questions don't have a fixed number of hops: "is this vendor connected to that sanctioned entity through any chain of subsidiaries?" AdaGQL expresses this with a length quantifier on the edge, e.g. (:Vendor {id:'V1'})-[:SUBSIDIARY_OF*1..5]->(:Vendor {id:'V9'}), matching one to five hops of the same edge type chained together. Unbounded reachability drops the upper bound: -[:SUBSIDIARY_OF*]->. This single construct replaces what would otherwise be a recursive Common Table Expression (CTE) or a hand-rolled loop of self-joins in SQL, and it is the single biggest reason graph engines exist: variable-length path matching is native, indexed, and terminates on cycles automatically rather than looping forever. Filters A WHERE clause narrows matched patterns using node or edge properties, exactly as in SQL: MATCH (h:Host)-[:RUNS]->(s:Service) WHERE s.port = 443 AND h.criticality >= 3 RETURN h.hostname. Filters can also constrain the path itself, for example requiring every edge in a variable-length hop to satisfy a property (ALL(e IN relationships(p) WHERE e.confidence > 0.8)), which lets you exclude low-confidence links from a multi-hop reachability question without discarding the whole path syntax. Aggregation over paths Once a pattern matches, AdaGQL supports the same aggregate functions as SQL, namely COUNT, SUM, AVG, and COLLECT, but applied over matched paths rather than rows. MATCH (a:Analyst)-[:REVIEWED]->(i:Incident) RETURN a.name, COUNT(i) AS reviewed ORDER BY reviewed DESC counts incidents per analyst in one pattern, with no GROUP BY join fan-out to worry about, because each match is already a distinct, bounded subgraph rather than a cross-product row. This distinction matters most when a pattern spans a variable-length path: aggregating over a fixed-hop join requires knowing the join depth in advance, while AdaGQL's aggregate functions operate over however many paths the pattern actually matched, whatever their length, without the query author needing to predict or bound that shape beforehand. Combined with filters and variable-length paths from earlier in this module, aggregation is usually the last clause added once a pattern has already been validated to match the right shape on a small example. This distinction matters most when a pattern spans a variable-length path: aggregating over a fixed-hop join in SQL requires knowing the join depth in advance, while AdaGQL's aggregate functions operate over however many paths the pattern actually matched, whatever their length, without the query author needing to predict or bound that shape beforehand. Combined with filters and variable-length paths from earlier in this module, aggregation is usually the last clause added once a pattern has already been validated to match the right shape on a small example. Related CCI capabilities The Analyst-Incident-Host patterns used throughout this module's examples are not incidental: querying who reviewed what, and how incidents connect to hosts and each other, is the same class of question CCI's Global Cyber Incident Database (GCIDB) 1834 (https://www.cambridgecyberinternational.com/en/products/gcidb-1834/) is built to answer at scale, and AdaGQL's pattern matching is the natural query interface for that kind of connected incident and intelligence data. Traversal and paths. Traversal and paths. Explain reachability and shortest-path semantics and compute them with a traversal query.. Traversal, Reachability, Shortest path, Cycles Traversal Underneath every AdaGQL pattern is a traversal: the engine starts at one or more anchor nodes and walks adjacency lists edge by edge, exactly the way you would follow a chain of references by hand, except the CCI GraphDB does it with an index-backed frontier instead of loading whole tables. Two traversal strategies matter conceptually. Breadth-first traversal expands one hop at a time across every frontier node before going deeper, which is what you want when the question is about *distance* (how many hops). Depth-first traversal follows one branch to its end before backtracking, which is cheaper when the question is existential (does any path exist at all). AdaGQL's planner picks the strategy per query; understanding both lets you reason about why a query is slow or fast. Reachability "Is B reachable from A at all?" is the purest graph question, and the one relational joins answer worst. In Structured Query Language (SQL), reachability over an unknown number of hops needs a recursive Common Table Expression (CTE), which most engines evaluate iteratively and which can blow up on dense graphs or unbounded cycles. In AdaGQL, MATCH (a:Account {id:'A1'})-[:TRANSFERRED_TO*]->(b:Account {id:'A9'}) RETURN COUNT(*) > 0 AS reachable is both the specification and the executable query: the engine's traversal handles termination, cycle detection, and visited-set bookkeeping internally. Shortest path AdaGQL exposes shortest-path search as a first-class function rather than something you approximate with ORDER BY hops LIMIT 1 over all paths (which is correct but wasteful, since it still enumerates every path). SHORTEST_PATH((a:Account {id:'A1'}), (b:Account {id:'A9'}), [:TRANSFERRED_TO*]) asks the engine to run a shortest-path algorithm (Dijkstra-style when edges carry a weight property, Breadth-First Search (BFS)-style over hop count otherwise) and return only the minimal path, which is dramatically cheaper on large or densely connected graphs. Weighted shortest path adds a WEIGHT clause referencing an edge property, letting "shortest" mean "cheapest" or "fastest" rather than "fewest hops." Cycles Real graphs loop: A transfers to B, B to C, C back to A. Naive path enumeration over a cyclic graph without a bound can run forever or return infinitely many paths of increasing length. AdaGQL's traversal maintains a visited-edge (or visited-node, configurably) set per path by default, so a variable-length match terminates even on a cyclic graph, returning each simple path once. Detecting a cycle itself is a query: MATCH p = (a)-[:TRANSFERRED_TO*2..]->(a) RETURN p finds any node reachable back to itself in two or more hops, a pattern with no natural SQL equivalent shorter than a hand-written recursive procedure. Termination guarantees matter operationally, not just theoretically: a traversal engine that does not track visited state internally can be driven into an effectively infinite query by a single densely cyclic subgraph, turning what looks like an ordinary reachability question into a denial-of-service risk against the query engine itself. Termination guarantees matter operationally, not just theoretically: a traversal engine that does not track visited state internally can be driven into an effectively infinite query by a single densely cyclic subgraph, turning what looks like an ordinary reachability question into a denial-of-service risk against the query engine itself. AdaGQL's default visited-set tracking on variable-length matches is what makes it safe to expose reachability and shortest-path queries to less trusted callers without a separate, hand-maintained depth limit on every query. Related CCI capabilities Reachability and cycle detection are not academic exercises in this context: whether a chain of relationships loops back on itself, for example a laundering pattern or a compromised-account chain that eventually reaches back to its own origin, is exactly the kind of question CCI's Global Cyber Incident Database (GCIDB) 1834 (https://www.cambridgecyberinternational.com/en/products/gcidb-1834/) and related intelligence work must answer over real incident and actor graphs.