feat(gql): support composable boolean predicates

What does this MR do and why?

This lets GQL combine filters into nested Boolean expressions across nodes and single-hop relationships. Callers can group conditions, negate whole groups, and compose comparisons, membership, string matches, and null checks while preserving scope and access checks.

Stack

This targets !2742 (closed) (michaelusa/predicate-compiler), which targets main. The empty-list prerequisites in !2730 (merged) are merged.

Relates to #1311 and #1258.

Supported expression forms

Construct Example Behavior
AND with negation a.id = 1 AND NOT b.name = 'debug' NOT binds before AND.
Nested groups NOT (a.id = 1 AND NOT (b.id = 2 AND b.name = 'helper')) Preserves each group's meaning.
Membership NOT callee.name IN ['debug', 'trace'] Excludes listed values.
Comparisons NOT n.id >= 10 Negates equality, inequality, and ordered comparisons.
String matches NOT n.name STARTS WITH 'test_' Also supports negated CONTAINS and ENDS WITH.
Token matches NOT token_match(n.name, 'debug') Also supports all_tokens and any_tokens on indexed fields.
Null checks NOT n.name IS NULL Supports both null-check forms.
Repeated negation NOT NOT n.name = 'execute' Preserves double negation.
Cross-node groups NOT (caller.name = 'compile' AND callee.name = 'helper') Combines conditions from different nodes.
Node and edge groups NOT (callee.name = 'debug' AND call.target_id = 12) Combines node fields and named edge fields.
Property comparisons NOT caller.name = callee.name Compares two node fields.

The operators are AND and NOT; OR and XOR remain unsupported. Remote queries still need a separate selective anchor. Negated nonempty membership excludes nulls. Negated empty membership is true, including for null values. Negated groups cannot reference virtual fields, neighbors, shortest paths, or relationship lists. Edge-to-property comparisons are rejected by shared validation.

Testing

352 compiler tests, 227 local tests, and the full ClickHouse data-correctness suite passed after simplifying filter handling. Workspace Clippy and formatting passed. The query DSL pin is 12.1.16.

Performance Analysis

Positive scan filters retain their existing planning paths. Single-node groups can narrow scans; cross-node groups retain the joins they need. Tests cover predicate placement and returned rows. Runtime performance has not been benchmarked.

Agent context: compiler integration and coverage

The parent MR owns the shared expression model, validation, normalization, restriction, and physical binding. This MR lowers GQL syntax into that model and documents its use. Ordinary filters stay in their maps. Cross-node comparisons use expression leaves instead of a separate join-predicate representation. Shared validation checks both forms. Grouped predicates keep their structure through planning.

Native security cases live in data-correctness scenarios under expect.predicates and run the shared compiler pipeline. Plan-shape fixtures cover accepted plans; compiler tests cover depth and leaf limits. Plain and grouped edge filters share the existing catalog lookup in !2742 (closed); there is no separate catalog prerequisite. The frontend resolves both comparison aliases before shared validation, preserving source spans for undefined variables. The changed design documents pass Vale and Markdown lint. Repository-wide docs lint reports 14 existing Vale errors in unrelated files.

Edited by Michael Usachenko

Merge request reports

Loading
Loading