Skip to main content
Version: 0.3.36

Filtering Data

How to narrow a query to the nodes you want: by type, by place in the tree, by property value, and by combinations of those. Filtering is done in SQL; the JSON query endpoints at the end of this page cover the simple lookups that don't need a statement.

All examples use a blog workspace with pages under /posts.

By type and path

SELECT path FROM 'blog' WHERE node_type = 'raisin:Page';
SELECT * FROM 'blog' WHERE path = '/posts/post-1';
SELECT * FROM 'blog' WHERE id = $1;

Type and path equality are index lookups. node_type IN ('a', 'b') expands to one index scan per value.

By position in the hierarchy

PredicateMatches
CHILD_OF('/posts')direct children of /posts
DESCENDANT_OF('/posts')everything under /posts, any depth
DESCENDANT_OF('/posts', 2)descendants up to two levels down
PATH_STARTS_WITH(path, '/posts/')same as DESCENDANT_OF('/posts'), spelled as a prefix test
PARENT(path) = '/posts'direct children, via the parent path
DEPTH(path) = 1 or depth = 1nodes at a given depth
SELECT path FROM 'blog' WHERE CHILD_OF('/posts');

SELECT path FROM 'blog'
WHERE DESCENDANT_OF('/posts') AND properties->>'status'::String = 'published';

-- top-level nodes only
SELECT path FROM 'blog' WHERE depth = 1;

PARENT(path, 2) and ANCESTOR(path, n) return the path n levels up, and can be selected as columns. REFERENCES('blog:/about') matches nodes whose properties hold a reference to that node; the workspace prefix is required.

By property value

->> reads a property as text. Cast the key to compare as another type:

-- equality
SELECT path FROM 'blog' WHERE properties->>'status'::String = 'published';

-- not equal (a node without the property also matches)
SELECT path FROM 'blog' WHERE properties->>'status'::String != 'draft';

-- numeric comparison
SELECT path FROM 'blog' WHERE properties->>'views'::Integer > 300;
SELECT path FROM 'blog' WHERE properties->>'views'::Integer BETWEEN 300 AND 600;

-- boolean
SELECT path FROM 'blog' WHERE properties->>'featured'::Boolean = true;

-- membership
SELECT path FROM 'blog' WHERE properties->>'status'::String IN ('draft', 'archived');
SELECT path FROM 'blog' WHERE properties->>'status'::String NOT IN ('draft');

-- date strings in ISO 8601 order correctly as text
SELECT path FROM 'blog' WHERE properties->>'published_at'::String > '2026-02-15';

-- relative to now: NOW() / CURRENT_TIMESTAMP and INTERVAL arithmetic work directly
SELECT path FROM 'blog' WHERE properties->>'published_at'::String < NOW() - INTERVAL '30 days';
SELECT path FROM 'blog' WHERE updated_at >= NOW() - INTERVAL '7 days';

!= and NOT treat a missing property as "not equal", so a row without status is returned by != 'draft'. Add IS NOT NULL if you only want rows that have the property.

Text matching

SELECT path FROM 'blog' WHERE properties->>'title'::String LIKE 'Post%';
SELECT path FROM 'blog' WHERE properties->>'title'::String ILIKE '%database%';
SELECT path FROM 'blog' WHERE name LIKE 'post-%';

Use the ::String cast with LIKE; the uncast form is routed to the equality index and does not evaluate patterns correctly. For word-level search across many nodes use full-text search instead of LIKE.

Regex operators are supported too — ~ (match), ~* (case-insensitive), !~ / !~* (negated), and SIMILAR TO (anchored SQL pattern):

SELECT path FROM 'blog' WHERE properties->>'phone'::String ~ '^\+1\d{10}$';
SELECT path FROM 'blog' WHERE properties->>'slug'::String !~ '[A-Z]';
SELECT path FROM 'blog' WHERE properties->>'title'::String SIMILAR TO 'Guide%';

Missing and present

SELECT path FROM 'blog' WHERE properties->>'featured_image' IS NULL;
SELECT path FROM 'blog' WHERE properties->>'views'::String IS NOT NULL;
SELECT path FROM 'blog' WHERE JSON_EXISTS(properties, '$.featured');

Arrays and nested objects

-- array contains a value
SELECT path FROM 'blog' WHERE properties->'tags' @> '["tech"]'::jsonb;

-- several key/value pairs at once
SELECT path FROM 'blog' WHERE properties @> '{"status": "published", "category": "tech"}'::jsonb;

-- nested value: step into the object with ->, read the leaf with ->>
SELECT path FROM 'blog' WHERE properties->'author'->>'name' = 'Jane';
SELECT path FROM 'blog' WHERE JSON_VALUE(properties, '$.author.name') = 'Jane';

= ANY(...) / <> ALL(...) compare a value against every element of an array:

SELECT path FROM 'blog' WHERE 'tech' = ANY(properties->'tags');
SELECT path FROM 'blog' WHERE node_type = ANY(ARRAY['blog:Post', 'blog:Page']);

Combining predicates

AND, OR, NOT and parentheses work as usual:

SELECT path FROM 'blog'
WHERE node_type = 'raisin:Page'
AND CHILD_OF('/posts')
AND (properties->>'status'::String = 'published'
OR properties->>'featured'::Boolean = true)
AND NOT (properties->>'category'::String = 'archive');

The planner picks one index (path prefix, node type, property equality or a compound index) as the access path and applies the remaining predicates as a filter. EXPLAIN shows which one was chosen:

EXPLAIN SELECT path FROM 'blog'
WHERE CHILD_OF('/posts') AND properties->>'status'::String = 'published';
-- PrefixScan: prefix=/posts/ with a Filter on top

Bound parameters

Use $1, $2, … and pass values in params. Parameters work anywhere a literal does, including LIMIT:

SELECT path FROM 'blog'
WHERE node_type = $1 AND properties->>'status'::String = $2
LIMIT $3;
{"sql": "...", "params": ["raisin:Page", "published", 20]}

JSON query endpoints

Two REST endpoints answer simple lookups without SQL. Both return a page object:

{"items": [ /* nodes */ ], "page": {"total": 6, "limit": 20, "offset": 0, "nextOffset": null}}

Lookup by path, parent or type

POST /api/repository/{repo}/{branch}/head/{workspace}/query takes exactly one of path, parent or nodeType (also accepted as node_type), plus limit and offset. parent is the parent node's id, not its path.

curl -X POST http://localhost:8090/api/repository/myrepo/main/head/blog/query \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"nodeType": "raisin:Page", "limit": 20}'

Results are sorted by path. Combining parent with nodeType filters the parent's children by type; anything else needs SQL.

Filter DSL

POST /api/repository/{repo}/{branch}/head/{workspace}/query/dsl accepts and / or / not trees over the top-level fields id, name, path, node_type and parent, with the operators eq, ne, like (substring), contains, in, exists, gt, lt, gte, lte, plus order_by, limit and offset:

{
"and": [
{ "node_type": { "eq": "raisin:Page" } },
{ "name": { "like": "ab" } }
],
"order_by": { "path": "asc" },
"limit": 5
}

The DSL evaluates only the workspace's root-level nodes and does not see properties. For property filters, hierarchy predicates or anything below the root, use SQL.

Next Steps