Skip to content

feat(spring-data): hierarchy operators (overlaps / ancestorOf / descendentOf)#249

Open
alexolivier wants to merge 1 commit into
spring-datafrom
spring-data-hierarchy
Open

feat(spring-data): hierarchy operators (overlaps / ancestorOf / descendentOf)#249
alexolivier wants to merge 1 commit into
spring-datafrom
spring-data-hierarchy

Conversation

@alexolivier

Copy link
Copy Markdown
Contributor

Stacked on #220 (base: spring-data). Adds the hierarchy operators that were listed as a follow-up in #220, bringing the Spring Data adapter to parity with the Prisma adapter.

What

A Cerbos hierarchy is a delimited path (e.g. a:b:c). Each side of a hierarchy operator is wrapped in a hierarchy(...) expression resolving to one of:

  • a constant delimited string split into segments,
  • a field whose column holds the whole delimited string, or
  • a list(...) of segments, each a constant or a field.
Operator Meaning JPA Criteria translation
ancestorOf(A, B) A is a strict prefix of B constant→field: B LIKE 'A.%'; field→constant: A IN (strictPrefixes(B))
descendentOf(A, B) B is a strict prefix of A symmetric to ancestorOf with operands swapped
overlaps(A, B) one is a prefix of the other segment-wise equality, or for a whole-column field: field IN (prefixes) OR field = raw OR field LIKE 'raw.%'

Constant-only operands collapse to an always-true (cb.conjunction()) or always-false (cb.disjunction()) predicate. LIKE prefixes reuse the same _/%/\ escaping as the string operators. Mirrors the Prisma adapter's algorithm so behaviour is consistent across adapters.

Tests

  • 11 unit tests (SpringDataQueryPlanAdapterTest.HierarchyOperators) covering every constant/field/segmented combination, the always-true/false collapses, strict-prefix IN, and the error contracts (non-hierarchy operands, two field-reference hierarchies, unsatisfiable constants).
  • 3 integration tests (SpringDataIntegrationTest.HierarchyOperators) against a real Cerbos PDP exercising the hierarchy-overlaps / hierarchy-ancestor-of / hierarchy-descendent-of policy actions in /policies/resource.yaml — this validates that the operator names, hierarchy(...)/list(...) shapes, R.id, and principal-attribute substitution all match what the planner emits.
  • Adds a scope column to the test ResourceEntity for the ancestor/descendent fixtures.

186 tests pass (incl. 98 integration tests against a real PDP), verified locally via the gradle Docker image with Testcontainers.

Once #220 merges, this can be retargeted to main.

alexolivier added a commit that referenced this pull request Jun 24, 2026
…ent MySQL LIKE escaping

- Add a StatementInspector-backed integration test that runs the three-hop
  deep-nested-category-label policy (categories -> subCategories -> labels) and
  asserts the generated SQL has three nested correlated EXISTS subqueries and no
  cross join — a cartesian join would return rows but pair unrelated children.
- README: document the MySQL/MariaDB LIKE backslash double-escaping gotcha and the
  NO_BACKSLASH_ESCAPES / OperatorFunction-override workarounds.

Addresses the remaining #220 follow-ups (hierarchy operators handled in #249).

Signed-off-by: Alex Olivier <alex@alexolivier.me>
…escendentOf)

Port the hierarchy operators from the Prisma adapter. A hierarchy(...) operand
resolves to a constant delimited path, a whole-column field, or a list(...) of
constant/field segments; the operators translate to:

- ancestorOf(A, B): A is a strict prefix of B
- descendentOf(A, B): B is a strict prefix of A
- overlaps(A, B): one is a prefix of the other

mapped onto cb.equal / path.in(strictPrefixes) / LIKE 'prefix%' (with the same
literal escaping as the string operators), collapsing constant-only cases to an
always-true/false predicate.

Tests: 11 unit tests (all type combinations + error contracts) and 3 integration
tests against a real Cerbos PDP exercising the hierarchy-overlaps/-ancestor-of/
-descendent-of policy actions. Adds a 'scope' column to the test ResourceEntity.

186 tests pass (incl. 98 integration tests against a real PDP).

Signed-off-by: Alex Olivier <alex@alexolivier.me>
@alexolivier alexolivier force-pushed the spring-data-hierarchy branch from fea6016 to 70e8d7f Compare June 29, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant