# Filters And Aggregation
Traversal filters are JSONB values produced by helper functions or by equivalent
JSON. Aggregation uses strict JSON traversal specs and aggregation specs.
## Register Filter Columns First
```sql
SELECT graph.add_filter_column('public.users'::regclass, 'age', 'numeric');
SELECT graph.add_filter_column('public.users'::regclass, 'active', 'boolean');
SELECT graph.add_filter_column('public.users'::regclass, 'state', 'text');
SELECT * FROM graph.build();
```
Filter columns are loaded into `FilterIndex` at build time. If you add or change
filter-column registration, rebuild before using it in traversal.
## Filter Helper Functions
| Function | Meaning |
|---|---|
| `graph.equals(column, value)` / `graph.eq(column, value)` | equality |
| `graph.not_equals(column, value)` / `graph.neq(column, value)` | inequality |
| `graph.greater_than(column, value)` / `graph.gt(column, value)` | greater than |
| `graph.at_least(column, value)` / `graph.gte(column, value)` | greater than or equal |
| `graph.less_than(column, value)` / `graph.lt(column, value)` | less than |
| `graph.at_most(column, value)` / `graph.lte(column, value)` | less than or equal |
| `graph.between(column, lower, upper)` | inclusive range |
| `graph.all(filters)` | logical AND merge |
| `graph.on_node(filter)` | wrap helper output under the traversal node scope |
Examples:
```sql
SELECT *
FROM graph.traverse(
'public.users'::regclass,
'u1',
max_depth := 2,
filter := graph.all(ARRAY[
graph.gte('age', 40),
graph.neq('state', '"inactive"'::jsonb)
]),
hydrate := true
);
```
```sql
SELECT *
FROM graph.find_related(
'name',
'Alice',
where_node := graph.gt('age', 40),
target_table := 'public.users'::regclass
);
```
The helper overloads accept convenient scalar text and integer values for common
cases, but JSONB values are the general representation.
## Filter Semantics
Filters are ANDed. A node must satisfy every predicate to be included.
JSON numbers compare as numbers. Integer values are compared exactly across the
signed and unsigned 64-bit range; decimal values use numeric fallback semantics.
JSON strings remain strings, so `"123"` does not match the number `123`.
Missing or NULL values:
| Operator family | NULL behavior |
|---|---|
| Equality and comparison | Does not match |
| `eq` with JSON `null` | Matches absent value |
| `neq` with JSON `null` | Matches present value |
The public helper set currently focuses on equality, inequality, comparisons,
and ranges. For NULL checks, pass JSON `null` explicitly:
```sql
SELECT *
FROM graph.traverse(
'public.users'::regclass,
'u1',
filter := graph.eq('deleted_at', 'null'::jsonb)
);
```
## Path Formatting Helpers
`graph.node_ref_string()` returns a canonical JSON array string used by strict
aggregation traversal specs:
```sql
SELECT graph.node_ref_string('public.users'::regclass, 'u1');
```
`graph.format_path()` converts a traversal `path` and `edge_path` pair to text:
```sql
SELECT graph.format_path(
'[{"table":"users","id":"u1"},{"table":"users","id":"u2"}]'::jsonb,
'["friend"]'::jsonb
);
```
Result:
```text
users:u1 --friend--> users:u2
```
## Path Count Estimate
`graph.path_count_estimate()` evaluates a strict traversal spec and returns:
| Column | Meaning |
|---|---|
| `estimated_paths` | Count up to `graph.max_exact_path_count` |
| `exact` | Whether the count completed exactly |
| `capped` | Whether counting stopped at the configured cap |
Example:
```sql
WITH req AS (
SELECT jsonb_build_object(
'starts',
jsonb_build_array(graph.node_ref_string('public.users'::regclass, 'u1')),
'direction', 'out',
'min_depth', 0,
'max_depth', 2,
'edge_types', jsonb_build_array('friend'),
'node_tables', jsonb_build_array('public.users')
) AS traversal
)
SELECT *
FROM graph.path_count_estimate((SELECT traversal FROM req));
```
## Aggregation
`graph.aggregate(traversal, aggregations, scope, path_limit)` runs server-side
aggregates over traversal results.
```sql
WITH req AS (
SELECT jsonb_build_object(
'starts',
jsonb_build_array(graph.node_ref_string('public.users'::regclass, 'u1')),
'direction', 'out',
'min_depth', 0,
'max_depth', 1,
'edge_types', jsonb_build_array('friend'),
'node_tables', jsonb_build_array('public.users')
) AS traversal
)
SELECT graph.aggregate(
traversal,
'{
"sum":[{"table":"public.users","column":"age","as":"total_age"}],
"avg":[{"table":"public.users","column":"age","as":"avg_age"}],
"count":[{"table":"public.users","column":"id","as":"user_count"}]
}'::jsonb
)
FROM req;
```
Supported aggregation groups:
| Group | Meaning |
|---|---|
| `sum` | Sum numeric column values |
| `avg` | Average numeric column values |
| `count` | Count non-null column values |
Each spec object contains:
| Key | Meaning |
|---|---|
| `table` | Source table regclass text |
| `column` | Source column to read |
| `as` | Output JSON key |
## Aggregation Scope
| Scope | Behavior |
|---|---|
| `returned_nodes` | Aggregate over the returned traversal node set |
| `chosen_parent_path` | Aggregate over the selected parent path to each result |
| `all_possible_paths` | Enumerate all possible paths under the traversal spec until `path_limit` |
Use `graph.path_count_estimate()` before `all_possible_paths` on broad graphs.
`all_possible_paths` can grow exponentially. The code enforces
`graph.max_exact_path_count` and the explicit `path_limit` to avoid unbounded
path enumeration.